@nerviq/cli 1.29.0 → 1.29.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +1527 -1493
  2. package/README.md +550 -538
  3. package/SECURITY.md +82 -82
  4. package/bin/cli.js +2562 -2558
  5. package/docs/api-reference.md +356 -356
  6. package/docs/audit-fix.md +109 -0
  7. package/docs/autofix.md +3 -62
  8. package/docs/getting-started.md +1 -1
  9. package/docs/index.html +592 -592
  10. package/docs/integration-contracts.md +287 -287
  11. package/docs/maintenance.md +128 -128
  12. package/docs/new-platform-guide.md +202 -202
  13. package/docs/release-process.md +63 -0
  14. package/docs/shallow-risk.md +244 -244
  15. package/docs/why-nerviq.md +82 -82
  16. package/package.json +67 -67
  17. package/src/aider/activity.js +226 -226
  18. package/src/aider/context.js +162 -162
  19. package/src/aider/freshness.js +123 -123
  20. package/src/aider/techniques.js +3465 -3465
  21. package/src/audit/layers.js +180 -180
  22. package/src/audit.js +1032 -1032
  23. package/src/benchmark.js +299 -299
  24. package/src/codex/activity.js +324 -324
  25. package/src/codex/freshness.js +142 -142
  26. package/src/codex/techniques.js +4895 -4895
  27. package/src/context.js +326 -326
  28. package/src/continuous-ops.js +11 -1
  29. package/src/convert.js +340 -340
  30. package/src/copilot/config-parser.js +280 -280
  31. package/src/copilot/context.js +218 -218
  32. package/src/copilot/freshness.js +177 -177
  33. package/src/copilot/patch.js +238 -238
  34. package/src/copilot/techniques.js +3578 -3578
  35. package/src/cursor/freshness.js +194 -194
  36. package/src/cursor/patch.js +243 -243
  37. package/src/cursor/techniques.js +3735 -3735
  38. package/src/doctor.js +201 -201
  39. package/src/fix-engine.js +511 -8
  40. package/src/formatters/csv.js +86 -86
  41. package/src/formatters/junit.js +123 -123
  42. package/src/formatters/markdown.js +164 -164
  43. package/src/formatters/otel.js +151 -151
  44. package/src/freshness.js +156 -156
  45. package/src/gemini/activity.js +402 -402
  46. package/src/gemini/context.js +290 -290
  47. package/src/gemini/freshness.js +183 -183
  48. package/src/gemini/patch.js +229 -229
  49. package/src/gemini/techniques.js +3811 -3811
  50. package/src/governance.js +533 -533
  51. package/src/harmony/audit.js +306 -306
  52. package/src/i18n.js +63 -63
  53. package/src/insights.js +119 -119
  54. package/src/integrations.js +134 -134
  55. package/src/locales/en.json +33 -33
  56. package/src/locales/es.json +33 -33
  57. package/src/migrate.js +354 -354
  58. package/src/opencode/activity.js +286 -286
  59. package/src/opencode/freshness.js +137 -137
  60. package/src/opencode/techniques.js +3450 -3450
  61. package/src/setup/analysis.js +12 -12
  62. package/src/setup.js +7 -6
  63. package/src/shallow-risk/index.js +56 -56
  64. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -50
  65. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -46
  66. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -46
  67. package/src/shallow-risk/patterns/agent-config-missing-file.js +317 -317
  68. package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -49
  69. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -34
  70. package/src/shallow-risk/patterns/hook-script-missing.js +70 -70
  71. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -52
  72. package/src/shallow-risk/shared.js +648 -648
  73. package/src/source-urls.js +295 -295
  74. package/src/state-paths.js +85 -85
  75. package/src/supplemental-checks.js +805 -805
  76. package/src/telemetry.js +160 -160
  77. package/src/windsurf/context.js +359 -359
  78. package/src/windsurf/freshness.js +194 -194
  79. package/src/windsurf/patch.js +231 -231
  80. package/src/windsurf/techniques.js +3779 -3779
@@ -1,164 +1,164 @@
1
- /**
2
- * Markdown Formatter
3
- *
4
- * Converts a nerviq audit result into GitHub-flavoured markdown suitable
5
- * for posting as a PR comment. Structure:
6
- *
7
- * - Header with score badge and pass/fail/skip counts
8
- * - Top 5 topNextActions as a GitHub task-list checklist
9
- * - Collapsible <details> block with the full failed-checks table
10
- * - Footer with Nerviq link, version, timestamp
11
- *
12
- * Output is plain GitHub-flavoured markdown. The only HTML used is
13
- * <details>/<summary>, which GitHub renders natively.
14
- */
15
-
16
- 'use strict';
17
-
18
- const { version: nerviqVersion } = require('../../package.json');
19
- const { SHALLOW_RISK_BANNER_LINES } = require('../shallow-risk');
20
-
21
- function escapeCell(value) {
22
- if (value === null || value === undefined) return '';
23
- return String(value)
24
- .replace(/\r?\n/g, ' ')
25
- .replace(/\|/g, '\\|');
26
- }
27
-
28
- function escapeInline(value) {
29
- if (value === null || value === undefined) return '';
30
- return String(value).replace(/\r?\n/g, ' ');
31
- }
32
-
33
- function scoreBadge(score) {
34
- const s = Number.isFinite(score) ? Math.round(score) : 0;
35
- let color;
36
- if (s >= 80) color = 'brightgreen';
37
- else if (s >= 60) color = 'yellow';
38
- else color = 'red';
39
- return `![score](https://img.shields.io/badge/nerviq_score-${s}%2F100-${color})`;
40
- }
41
-
42
- function severityFor(item) {
43
- return item.severity || item.impact || 'medium';
44
- }
45
-
46
- function formatMarkdown(auditResult, options = {}) {
47
- const platformLabel = auditResult.platformLabel || auditResult.platform || 'claude';
48
- const score = auditResult.score ?? 0;
49
- const passed = auditResult.passed ?? 0;
50
- const failed = auditResult.failed ?? 0;
51
- const skipped = auditResult.skipped ?? 0;
52
- const version = auditResult.version || nerviqVersion;
53
- const timestamp = auditResult.timestamp || new Date().toISOString();
54
-
55
- const lines = [];
56
-
57
- lines.push(`## Score: ${score}/100 ${scoreBadge(score)}`);
58
- lines.push('');
59
- lines.push(`**Platform:** ${platformLabel} `);
60
- lines.push(`**Checks:** ${passed} passed, ${failed} failed, ${skipped} skipped`);
61
- lines.push('');
62
-
63
- const top = Array.isArray(auditResult.topNextActions)
64
- ? auditResult.topNextActions.slice(0, 5)
65
- : [];
66
-
67
- if (top.length > 0) {
68
- lines.push('### Top next actions');
69
- lines.push('');
70
- for (const item of top) {
71
- const sev = severityFor(item).toString().toUpperCase();
72
- const title = escapeInline(item.name || item.title || item.key);
73
- const key = escapeInline(item.key || item.id || '');
74
- let loc = '';
75
- if (item.file) {
76
- loc = ` — \`${escapeInline(item.file)}${item.line ? ':' + item.line : ''}\``;
77
- }
78
- let delta = '';
79
- if (Number.isFinite(item.projectedScoreDelta) && item.projectedScoreDelta > 0) {
80
- delta = ` (+${item.projectedScoreDelta} pts → ${item.projectedScoreAfter}/100)`;
81
- }
82
- // CTO-08: include scope layer as a small suffix after the key so
83
- // evaluators see which layer each next-action belongs to.
84
- const layerSuffix = item.layer ? ` · _layer: ${escapeInline(item.layer)}_` : '';
85
- lines.push(`- [ ] **[${sev}] ${title}** (\`${key}\`)${loc}${delta}${layerSuffix}`);
86
- const hint = item.fix || item.hint || '';
87
- if (hint) {
88
- lines.push(` - ${escapeInline(hint)}`);
89
- }
90
- if (item.snippet) {
91
- lines.push('');
92
- lines.push(' ```');
93
- for (const snipLine of String(item.snippet).split(/\r?\n/)) {
94
- lines.push(` ${snipLine}`);
95
- }
96
- lines.push(' ```');
97
- }
98
- }
99
- lines.push('');
100
- }
101
-
102
- if (Array.isArray(auditResult.shallowRiskHints)) {
103
- lines.push('### Shallow Risk (experimental, opt-in)');
104
- lines.push('');
105
- for (const line of SHALLOW_RISK_BANNER_LINES) {
106
- lines.push(`> ${line}`);
107
- }
108
- lines.push('');
109
-
110
- if (auditResult.shallowRiskHints.length === 0) {
111
- lines.push('_No shallow-risk hints found._');
112
- lines.push('');
113
- } else {
114
- for (const item of auditResult.shallowRiskHints) {
115
- const sev = severityFor(item).toString().toUpperCase();
116
- const title = escapeInline(item.name || item.title || item.key);
117
- let loc = '';
118
- if (item.file) {
119
- loc = ` — \`${escapeInline(item.file)}${item.line ? ':' + item.line : ''}\``;
120
- }
121
- lines.push(`- **[${sev}] ${title}** (\`${escapeInline(item.key || '')}\`)${loc}`);
122
- if (item.fix) {
123
- lines.push(` - ${escapeInline(item.fix)}`);
124
- }
125
- }
126
- lines.push('');
127
- }
128
- }
129
-
130
- const failedResults = Array.isArray(auditResult.results)
131
- ? auditResult.results.filter((r) => r.passed === false)
132
- : [];
133
-
134
- if (failedResults.length > 0) {
135
- lines.push('<details>');
136
- lines.push(`<summary>All failed checks (${failedResults.length})</summary>`);
137
- lines.push('');
138
- // CTO-08: add layer column between category and rating.
139
- lines.push('| key | name | category | layer | rating | file | line |');
140
- lines.push('| --- | --- | --- | --- | --- | --- | --- |');
141
- for (const r of failedResults) {
142
- const row = [
143
- escapeCell(r.key),
144
- escapeCell(r.name),
145
- escapeCell(r.category),
146
- escapeCell(r.layer || ''),
147
- escapeCell(r.rating ?? ''),
148
- escapeCell(r.file || ''),
149
- escapeCell(r.line || ''),
150
- ];
151
- lines.push(`| ${row.join(' | ')} |`);
152
- }
153
- lines.push('');
154
- lines.push('</details>');
155
- lines.push('');
156
- }
157
-
158
- lines.push(`---`);
159
- lines.push(`Generated by [Nerviq](https://nerviq.net) v${version} · ${timestamp}`);
160
-
161
- return lines.join('\n');
162
- }
163
-
164
- module.exports = { formatMarkdown };
1
+ /**
2
+ * Markdown Formatter
3
+ *
4
+ * Converts a nerviq audit result into GitHub-flavoured markdown suitable
5
+ * for posting as a PR comment. Structure:
6
+ *
7
+ * - Header with score badge and pass/fail/skip counts
8
+ * - Top 5 topNextActions as a GitHub task-list checklist
9
+ * - Collapsible <details> block with the full failed-checks table
10
+ * - Footer with Nerviq link, version, timestamp
11
+ *
12
+ * Output is plain GitHub-flavoured markdown. The only HTML used is
13
+ * <details>/<summary>, which GitHub renders natively.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { version: nerviqVersion } = require('../../package.json');
19
+ const { SHALLOW_RISK_BANNER_LINES } = require('../shallow-risk');
20
+
21
+ function escapeCell(value) {
22
+ if (value === null || value === undefined) return '';
23
+ return String(value)
24
+ .replace(/\r?\n/g, ' ')
25
+ .replace(/\|/g, '\\|');
26
+ }
27
+
28
+ function escapeInline(value) {
29
+ if (value === null || value === undefined) return '';
30
+ return String(value).replace(/\r?\n/g, ' ');
31
+ }
32
+
33
+ function scoreBadge(score) {
34
+ const s = Number.isFinite(score) ? Math.round(score) : 0;
35
+ let color;
36
+ if (s >= 80) color = 'brightgreen';
37
+ else if (s >= 60) color = 'yellow';
38
+ else color = 'red';
39
+ return `![score](https://img.shields.io/badge/nerviq_score-${s}%2F100-${color})`;
40
+ }
41
+
42
+ function severityFor(item) {
43
+ return item.severity || item.impact || 'medium';
44
+ }
45
+
46
+ function formatMarkdown(auditResult, options = {}) {
47
+ const platformLabel = auditResult.platformLabel || auditResult.platform || 'claude';
48
+ const score = auditResult.score ?? 0;
49
+ const passed = auditResult.passed ?? 0;
50
+ const failed = auditResult.failed ?? 0;
51
+ const skipped = auditResult.skipped ?? 0;
52
+ const version = auditResult.version || nerviqVersion;
53
+ const timestamp = auditResult.timestamp || new Date().toISOString();
54
+
55
+ const lines = [];
56
+
57
+ lines.push(`## Score: ${score}/100 ${scoreBadge(score)}`);
58
+ lines.push('');
59
+ lines.push(`**Platform:** ${platformLabel} `);
60
+ lines.push(`**Checks:** ${passed} passed, ${failed} failed, ${skipped} skipped`);
61
+ lines.push('');
62
+
63
+ const top = Array.isArray(auditResult.topNextActions)
64
+ ? auditResult.topNextActions.slice(0, 5)
65
+ : [];
66
+
67
+ if (top.length > 0) {
68
+ lines.push('### Top next actions');
69
+ lines.push('');
70
+ for (const item of top) {
71
+ const sev = severityFor(item).toString().toUpperCase();
72
+ const title = escapeInline(item.name || item.title || item.key);
73
+ const key = escapeInline(item.key || item.id || '');
74
+ let loc = '';
75
+ if (item.file) {
76
+ loc = ` — \`${escapeInline(item.file)}${item.line ? ':' + item.line : ''}\``;
77
+ }
78
+ let delta = '';
79
+ if (Number.isFinite(item.projectedScoreDelta) && item.projectedScoreDelta > 0) {
80
+ delta = ` (+${item.projectedScoreDelta} pts → ${item.projectedScoreAfter}/100)`;
81
+ }
82
+ // CTO-08: include scope layer as a small suffix after the key so
83
+ // evaluators see which layer each next-action belongs to.
84
+ const layerSuffix = item.layer ? ` · _layer: ${escapeInline(item.layer)}_` : '';
85
+ lines.push(`- [ ] **[${sev}] ${title}** (\`${key}\`)${loc}${delta}${layerSuffix}`);
86
+ const hint = item.fix || item.hint || '';
87
+ if (hint) {
88
+ lines.push(` - ${escapeInline(hint)}`);
89
+ }
90
+ if (item.snippet) {
91
+ lines.push('');
92
+ lines.push(' ```');
93
+ for (const snipLine of String(item.snippet).split(/\r?\n/)) {
94
+ lines.push(` ${snipLine}`);
95
+ }
96
+ lines.push(' ```');
97
+ }
98
+ }
99
+ lines.push('');
100
+ }
101
+
102
+ if (Array.isArray(auditResult.shallowRiskHints)) {
103
+ lines.push('### Shallow Risk (experimental, opt-in)');
104
+ lines.push('');
105
+ for (const line of SHALLOW_RISK_BANNER_LINES) {
106
+ lines.push(`> ${line}`);
107
+ }
108
+ lines.push('');
109
+
110
+ if (auditResult.shallowRiskHints.length === 0) {
111
+ lines.push('_No shallow-risk hints found._');
112
+ lines.push('');
113
+ } else {
114
+ for (const item of auditResult.shallowRiskHints) {
115
+ const sev = severityFor(item).toString().toUpperCase();
116
+ const title = escapeInline(item.name || item.title || item.key);
117
+ let loc = '';
118
+ if (item.file) {
119
+ loc = ` — \`${escapeInline(item.file)}${item.line ? ':' + item.line : ''}\``;
120
+ }
121
+ lines.push(`- **[${sev}] ${title}** (\`${escapeInline(item.key || '')}\`)${loc}`);
122
+ if (item.fix) {
123
+ lines.push(` - ${escapeInline(item.fix)}`);
124
+ }
125
+ }
126
+ lines.push('');
127
+ }
128
+ }
129
+
130
+ const failedResults = Array.isArray(auditResult.results)
131
+ ? auditResult.results.filter((r) => r.passed === false)
132
+ : [];
133
+
134
+ if (failedResults.length > 0) {
135
+ lines.push('<details>');
136
+ lines.push(`<summary>All failed checks (${failedResults.length})</summary>`);
137
+ lines.push('');
138
+ // CTO-08: add layer column between category and rating.
139
+ lines.push('| key | name | category | layer | rating | file | line |');
140
+ lines.push('| --- | --- | --- | --- | --- | --- | --- |');
141
+ for (const r of failedResults) {
142
+ const row = [
143
+ escapeCell(r.key),
144
+ escapeCell(r.name),
145
+ escapeCell(r.category),
146
+ escapeCell(r.layer || ''),
147
+ escapeCell(r.rating ?? ''),
148
+ escapeCell(r.file || ''),
149
+ escapeCell(r.line || ''),
150
+ ];
151
+ lines.push(`| ${row.join(' | ')} |`);
152
+ }
153
+ lines.push('');
154
+ lines.push('</details>');
155
+ lines.push('');
156
+ }
157
+
158
+ lines.push(`---`);
159
+ lines.push(`Generated by [Nerviq](https://nerviq.net) v${version} · ${timestamp}`);
160
+
161
+ return lines.join('\n');
162
+ }
163
+
164
+ module.exports = { formatMarkdown };
@@ -1,151 +1,151 @@
1
- /**
2
- * OpenTelemetry Metrics Formatter
3
- *
4
- * Converts a nerviq audit result into an OpenTelemetry-compatible
5
- * metrics export format (OTLP JSON, metrics signal).
6
- *
7
- * Metrics emitted:
8
- * nerviq.audit.score — gauge 0-100
9
- * nerviq.audit.checks.passed — gauge count of passing checks
10
- * nerviq.audit.checks.failed — gauge count of failing checks
11
- * nerviq.audit.checks.total — gauge total checks evaluated
12
- * nerviq.audit.duration_ms — gauge audit wall-clock time (if provided)
13
- *
14
- * Each metric is tagged with:
15
- * platform — e.g. "claude", "codex", "cursor"
16
- * version — nerviq package version
17
- *
18
- * Output conforms to OTLP ExportMetricsServiceRequest JSON structure
19
- * (opentelemetry-proto/collector/metrics/v1).
20
- */
21
-
22
- 'use strict';
23
-
24
- const { version: nerviqVersion } = require('../../package.json');
25
-
26
- // ─── Helpers ─────────────────────────────────────────────────────────────────
27
-
28
- function unixNanoNow() {
29
- // Returns current time as a bigint nanoseconds string for OTLP
30
- return String(BigInt(Date.now()) * 1_000_000n);
31
- }
32
-
33
- function makeResource(platform) {
34
- return {
35
- attributes: [
36
- { key: 'service.name', value: { stringValue: 'nerviq' } },
37
- { key: 'service.version', value: { stringValue: nerviqVersion } },
38
- { key: 'nerviq.platform', value: { stringValue: platform } },
39
- ],
40
- droppedAttributesCount: 0,
41
- };
42
- }
43
-
44
- function makeGauge(name, description, unit, value, attributes = [], timeUnixNano) {
45
- if (value === null || value === undefined) return null;
46
- return {
47
- name,
48
- description,
49
- unit,
50
- gauge: {
51
- dataPoints: [
52
- {
53
- attributes,
54
- startTimeUnixNano: timeUnixNano,
55
- timeUnixNano,
56
- asDouble: Number(value),
57
- },
58
- ],
59
- },
60
- };
61
- }
62
-
63
- // ─── Main formatter ───────────────────────────────────────────────────────────
64
-
65
- /**
66
- * Convert a nerviq audit result to an OTLP-compatible metrics payload.
67
- *
68
- * @param {object} auditResult — result from audit()
69
- * @param {number} [auditResult.score]
70
- * @param {number} [auditResult.passed]
71
- * @param {number} [auditResult.failed]
72
- * @param {number} [auditResult.total]
73
- * @param {string} [auditResult.platform]
74
- * @param {number} [auditResult.durationMs] — optional, set by caller
75
- * @returns {object} OTLP ExportMetricsServiceRequest JSON
76
- */
77
- function formatOtelMetrics(auditResult) {
78
- const platform = auditResult.platform || 'claude';
79
- const now = unixNanoNow();
80
-
81
- const sharedAttributes = [
82
- { key: 'nerviq.platform', value: { stringValue: platform } },
83
- { key: 'nerviq.version', value: { stringValue: nerviqVersion } },
84
- ];
85
-
86
- const metrics = [
87
- makeGauge(
88
- 'nerviq.audit.score',
89
- 'Nerviq audit score (0-100)',
90
- '1',
91
- auditResult.score,
92
- sharedAttributes,
93
- now,
94
- ),
95
- makeGauge(
96
- 'nerviq.audit.checks.passed',
97
- 'Number of checks that passed',
98
- '{checks}',
99
- auditResult.passed,
100
- sharedAttributes,
101
- now,
102
- ),
103
- makeGauge(
104
- 'nerviq.audit.checks.failed',
105
- 'Number of checks that failed',
106
- '{checks}',
107
- auditResult.failed,
108
- sharedAttributes,
109
- now,
110
- ),
111
- makeGauge(
112
- 'nerviq.audit.checks.total',
113
- 'Total number of checks evaluated',
114
- '{checks}',
115
- auditResult.total ?? ((auditResult.passed || 0) + (auditResult.failed || 0)),
116
- sharedAttributes,
117
- now,
118
- ),
119
- ].filter(Boolean);
120
-
121
- if (auditResult.durationMs != null) {
122
- const dm = makeGauge(
123
- 'nerviq.audit.duration_ms',
124
- 'Audit wall-clock duration in milliseconds',
125
- 'ms',
126
- auditResult.durationMs,
127
- sharedAttributes,
128
- now,
129
- );
130
- if (dm) metrics.push(dm);
131
- }
132
-
133
- return {
134
- resourceMetrics: [
135
- {
136
- resource: makeResource(platform),
137
- scopeMetrics: [
138
- {
139
- scope: {
140
- name: 'nerviq',
141
- version: nerviqVersion,
142
- },
143
- metrics,
144
- },
145
- ],
146
- },
147
- ],
148
- };
149
- }
150
-
151
- module.exports = { formatOtelMetrics };
1
+ /**
2
+ * OpenTelemetry Metrics Formatter
3
+ *
4
+ * Converts a nerviq audit result into an OpenTelemetry-compatible
5
+ * metrics export format (OTLP JSON, metrics signal).
6
+ *
7
+ * Metrics emitted:
8
+ * nerviq.audit.score — gauge 0-100
9
+ * nerviq.audit.checks.passed — gauge count of passing checks
10
+ * nerviq.audit.checks.failed — gauge count of failing checks
11
+ * nerviq.audit.checks.total — gauge total checks evaluated
12
+ * nerviq.audit.duration_ms — gauge audit wall-clock time (if provided)
13
+ *
14
+ * Each metric is tagged with:
15
+ * platform — e.g. "claude", "codex", "cursor"
16
+ * version — nerviq package version
17
+ *
18
+ * Output conforms to OTLP ExportMetricsServiceRequest JSON structure
19
+ * (opentelemetry-proto/collector/metrics/v1).
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const { version: nerviqVersion } = require('../../package.json');
25
+
26
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
27
+
28
+ function unixNanoNow() {
29
+ // Returns current time as a bigint nanoseconds string for OTLP
30
+ return String(BigInt(Date.now()) * 1_000_000n);
31
+ }
32
+
33
+ function makeResource(platform) {
34
+ return {
35
+ attributes: [
36
+ { key: 'service.name', value: { stringValue: 'nerviq' } },
37
+ { key: 'service.version', value: { stringValue: nerviqVersion } },
38
+ { key: 'nerviq.platform', value: { stringValue: platform } },
39
+ ],
40
+ droppedAttributesCount: 0,
41
+ };
42
+ }
43
+
44
+ function makeGauge(name, description, unit, value, attributes = [], timeUnixNano) {
45
+ if (value === null || value === undefined) return null;
46
+ return {
47
+ name,
48
+ description,
49
+ unit,
50
+ gauge: {
51
+ dataPoints: [
52
+ {
53
+ attributes,
54
+ startTimeUnixNano: timeUnixNano,
55
+ timeUnixNano,
56
+ asDouble: Number(value),
57
+ },
58
+ ],
59
+ },
60
+ };
61
+ }
62
+
63
+ // ─── Main formatter ───────────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Convert a nerviq audit result to an OTLP-compatible metrics payload.
67
+ *
68
+ * @param {object} auditResult — result from audit()
69
+ * @param {number} [auditResult.score]
70
+ * @param {number} [auditResult.passed]
71
+ * @param {number} [auditResult.failed]
72
+ * @param {number} [auditResult.total]
73
+ * @param {string} [auditResult.platform]
74
+ * @param {number} [auditResult.durationMs] — optional, set by caller
75
+ * @returns {object} OTLP ExportMetricsServiceRequest JSON
76
+ */
77
+ function formatOtelMetrics(auditResult) {
78
+ const platform = auditResult.platform || 'claude';
79
+ const now = unixNanoNow();
80
+
81
+ const sharedAttributes = [
82
+ { key: 'nerviq.platform', value: { stringValue: platform } },
83
+ { key: 'nerviq.version', value: { stringValue: nerviqVersion } },
84
+ ];
85
+
86
+ const metrics = [
87
+ makeGauge(
88
+ 'nerviq.audit.score',
89
+ 'Nerviq audit score (0-100)',
90
+ '1',
91
+ auditResult.score,
92
+ sharedAttributes,
93
+ now,
94
+ ),
95
+ makeGauge(
96
+ 'nerviq.audit.checks.passed',
97
+ 'Number of checks that passed',
98
+ '{checks}',
99
+ auditResult.passed,
100
+ sharedAttributes,
101
+ now,
102
+ ),
103
+ makeGauge(
104
+ 'nerviq.audit.checks.failed',
105
+ 'Number of checks that failed',
106
+ '{checks}',
107
+ auditResult.failed,
108
+ sharedAttributes,
109
+ now,
110
+ ),
111
+ makeGauge(
112
+ 'nerviq.audit.checks.total',
113
+ 'Total number of checks evaluated',
114
+ '{checks}',
115
+ auditResult.total ?? ((auditResult.passed || 0) + (auditResult.failed || 0)),
116
+ sharedAttributes,
117
+ now,
118
+ ),
119
+ ].filter(Boolean);
120
+
121
+ if (auditResult.durationMs != null) {
122
+ const dm = makeGauge(
123
+ 'nerviq.audit.duration_ms',
124
+ 'Audit wall-clock duration in milliseconds',
125
+ 'ms',
126
+ auditResult.durationMs,
127
+ sharedAttributes,
128
+ now,
129
+ );
130
+ if (dm) metrics.push(dm);
131
+ }
132
+
133
+ return {
134
+ resourceMetrics: [
135
+ {
136
+ resource: makeResource(platform),
137
+ scopeMetrics: [
138
+ {
139
+ scope: {
140
+ name: 'nerviq',
141
+ version: nerviqVersion,
142
+ },
143
+ metrics,
144
+ },
145
+ ],
146
+ },
147
+ ],
148
+ };
149
+ }
150
+
151
+ module.exports = { formatOtelMetrics };