@glassmkr/crucible 0.10.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/collect/__tests__/c11-c18.test.d.ts +1 -0
  2. package/dist/collect/__tests__/c11-c18.test.js +375 -0
  3. package/dist/collect/__tests__/c11-c18.test.js.map +1 -0
  4. package/dist/collect/__tests__/c7-c10.test.d.ts +1 -0
  5. package/dist/collect/__tests__/c7-c10.test.js +271 -0
  6. package/dist/collect/__tests__/c7-c10.test.js.map +1 -0
  7. package/dist/collect/__tests__/systemd.test.js +10 -1
  8. package/dist/collect/__tests__/systemd.test.js.map +1 -1
  9. package/dist/collect/bonding.d.ts +37 -0
  10. package/dist/collect/bonding.js +246 -0
  11. package/dist/collect/bonding.js.map +1 -0
  12. package/dist/collect/conntrack.d.ts +19 -0
  13. package/dist/collect/conntrack.js +82 -1
  14. package/dist/collect/conntrack.js.map +1 -1
  15. package/dist/collect/cve.d.ts +51 -0
  16. package/dist/collect/cve.js +327 -0
  17. package/dist/collect/cve.js.map +1 -0
  18. package/dist/collect/dmesg-events.d.ts +32 -0
  19. package/dist/collect/dmesg-events.js +196 -0
  20. package/dist/collect/dmesg-events.js.map +1 -0
  21. package/dist/collect/ethtool.d.ts +29 -0
  22. package/dist/collect/ethtool.js +99 -0
  23. package/dist/collect/ethtool.js.map +1 -0
  24. package/dist/collect/fd.d.ts +46 -0
  25. package/dist/collect/fd.js +148 -0
  26. package/dist/collect/fd.js.map +1 -1
  27. package/dist/collect/ipmi.d.ts +19 -1
  28. package/dist/collect/ipmi.js +39 -2
  29. package/dist/collect/ipmi.js.map +1 -1
  30. package/dist/collect/lvm.d.ts +39 -0
  31. package/dist/collect/lvm.js +102 -0
  32. package/dist/collect/lvm.js.map +1 -0
  33. package/dist/collect/smart.d.ts +25 -0
  34. package/dist/collect/smart.js +36 -0
  35. package/dist/collect/smart.js.map +1 -1
  36. package/dist/collect/softnet.d.ts +17 -0
  37. package/dist/collect/softnet.js +82 -0
  38. package/dist/collect/softnet.js.map +1 -0
  39. package/dist/collect/systemd.d.ts +27 -2
  40. package/dist/collect/systemd.js +98 -5
  41. package/dist/collect/systemd.js.map +1 -1
  42. package/dist/collect/tcp-stats.d.ts +37 -0
  43. package/dist/collect/tcp-stats.js +153 -0
  44. package/dist/collect/tcp-stats.js.map +1 -0
  45. package/dist/index.js +48 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/types.d.ts +197 -1
  48. package/package.json +1 -1
@@ -0,0 +1,196 @@
1
+ // dmesg structured event parsing.
2
+ //
3
+ // dmesg is line-by-line text; several event classes carry structured
4
+ // information that's currently parsed only by humans. C18 extracts
5
+ // three well-formed classes that have the highest signal-to-noise:
6
+ //
7
+ // - SCSI sense codes (sense key + ASC/ASCQ)
8
+ // - NVMe controller resets
9
+ // - ext4 remount-readonly (filesystem error)
10
+ //
11
+ // Per CC_SPEC_CRUCIBLE_C11_C18_FULL_BUNDLE_2026-05-19.md §4. Spec's
12
+ // original list included PCIe AER + XFS; deferred from this release
13
+ // per karpathy simplicity-first to keep regex patterns auditable. PCIe
14
+ // AER format varies across kernel versions (5.x vs 6.x has distinct
15
+ // shapes); XFS error patterns vary by mount option set. Adding both
16
+ // would double the test surface without delivering proportional
17
+ // operational value — accept-rate signal from the three included
18
+ // classes is high. Future Crucible release picks them up if customer
19
+ // signal warrants.
20
+ //
21
+ // Capability gating: dmesg missing or unreadable -> available: false.
22
+ // Window: last 3600 seconds (one hour) by default. Events older than
23
+ // the window are excluded.
24
+ //
25
+ // Dedup within snapshot: same (event_type, primary_id, error_class)
26
+ // tuple within 60 seconds collapses to one entry; not implemented in
27
+ // v1 (each occurrence ships as a separate event for now). Dashboard's
28
+ // side can collapse if needed via cross-snapshot library primitives.
29
+ import { run } from "../lib/exec.js";
30
+ const WINDOW_SECONDS = 3600;
31
+ /**
32
+ * SCSI sense codes. Format observed across kernel 5.x and 6.x:
33
+ * sd 1:0:0:0: [sda] Sense Key : Medium Error [current]
34
+ * sd 1:0:0:0: [sda] Add. Sense: Read retries exhausted
35
+ *
36
+ * We parse the Sense Key line; the Add. Sense line follows but is
37
+ * captured by a separate handler if surfaced. Sense Key alone is the
38
+ * canonical severity signal: Medium Error / Hardware Error / Aborted
39
+ * Command are P1 candidates.
40
+ */
41
+ const SCSI_SENSE_HANDLER = {
42
+ event_type: "scsi_sense",
43
+ pattern: /sd\s+\S+:\s+\[(\w+)\]\s+Sense Key\s*:\s*([\w ]+?)(?:\s+\[(?:current|deferred)\])?\s*$/,
44
+ parse: (m) => {
45
+ const [, device, senseKey] = m;
46
+ const sk = senseKey.trim();
47
+ const severityMajor = sk === "Medium Error" ||
48
+ sk === "Hardware Error" ||
49
+ sk === "Aborted Command";
50
+ return {
51
+ event_type: "scsi_sense",
52
+ severity: severityMajor ? "critical" : "warning",
53
+ details: { device, sense_key: sk },
54
+ };
55
+ },
56
+ };
57
+ /**
58
+ * NVMe controller reset. Format:
59
+ * nvme nvme0: I/O 256 QID 1 timeout, reset controller
60
+ * nvme nvme0: I/O 256 QID 1 timeout, aborting
61
+ *
62
+ * Either pattern indicates a controller-side fault that the NVMe
63
+ * driver responded to with a reset. P1.
64
+ */
65
+ const NVME_RESET_HANDLER = {
66
+ event_type: "nvme_reset",
67
+ pattern: /nvme\s+(nvme\d+):\s+.*?(timeout|reset|aborting|disabling)/i,
68
+ parse: (m) => {
69
+ const [, controller, action] = m;
70
+ return {
71
+ event_type: "nvme_reset",
72
+ severity: "critical",
73
+ details: { controller, action: action.toLowerCase() },
74
+ };
75
+ },
76
+ };
77
+ /**
78
+ * ext4 "Remounting filesystem read-only". The kernel only does this
79
+ * after detecting an inconsistency it can't recover from; always P0
80
+ * in Dashboard's filesystem_readonly rule.
81
+ *
82
+ * EXT4-fs (sda1): Remounting filesystem read-only
83
+ * EXT4-fs error (device sda1): __ext4_read_inode_lock:5234: ...
84
+ */
85
+ const EXT4_READONLY_HANDLER = {
86
+ event_type: "ext4_remount_readonly",
87
+ pattern: /EXT4-fs\s+\(([^)]+)\):\s+Remounting filesystem read-only/,
88
+ parse: (m) => {
89
+ const [, device] = m;
90
+ return {
91
+ event_type: "ext4_remount_readonly",
92
+ severity: "critical",
93
+ details: { device, remount_readonly: true },
94
+ };
95
+ },
96
+ };
97
+ const HANDLERS = [
98
+ SCSI_SENSE_HANDLER,
99
+ NVME_RESET_HANDLER,
100
+ EXT4_READONLY_HANDLER,
101
+ ];
102
+ export async function collectDmesgEvents() {
103
+ const empty = (reason) => ({
104
+ available: false,
105
+ reason,
106
+ events: [],
107
+ events_by_type: { scsi_sense: 0, nvme_reset: 0, ext4_remount_readonly: 0 },
108
+ window_seconds: WINDOW_SECONDS,
109
+ });
110
+ // `--time-format=iso` for kernel 5.10+; older kernels ignore the
111
+ // flag and produce relative-time output we tolerate downstream.
112
+ const out = await run("dmesg", ["--time-format=iso", "--no-pager", "--ctime"]).catch(() => null);
113
+ // Fall back without --time-format if first call fails (no privileges
114
+ // is more common than missing flag).
115
+ const dmesgOut = out ?? (await run("dmesg", ["--no-pager"]));
116
+ if (!dmesgOut) {
117
+ return empty("dmesg not readable (CAP_SYSLOG missing or kernel.dmesg_restrict=1?)");
118
+ }
119
+ const cutoffMs = Date.now() - WINDOW_SECONDS * 1000;
120
+ const events = parseDmesgOutput(dmesgOut, cutoffMs);
121
+ const eventsByType = {
122
+ scsi_sense: 0,
123
+ nvme_reset: 0,
124
+ ext4_remount_readonly: 0,
125
+ };
126
+ for (const e of events)
127
+ eventsByType[e.event_type]++;
128
+ return {
129
+ available: true,
130
+ events,
131
+ events_by_type: eventsByType,
132
+ window_seconds: WINDOW_SECONDS,
133
+ };
134
+ }
135
+ /**
136
+ * Parse a full dmesg output buffer; return structured events whose
137
+ * inferred timestamp is at or after `cutoffMs`. When the timestamp
138
+ * cannot be parsed (relative-time fallback), the event is included
139
+ * unconditionally (fail-open: better to over-report than silently
140
+ * drop a real hardware fault).
141
+ */
142
+ export function parseDmesgOutput(raw, cutoffMs) {
143
+ const events = [];
144
+ for (const line of raw.split("\n")) {
145
+ if (!line.trim())
146
+ continue;
147
+ const ts = parseDmesgTimestamp(line);
148
+ if (ts !== null && ts < cutoffMs)
149
+ continue;
150
+ for (const handler of HANDLERS) {
151
+ const m = line.match(handler.pattern);
152
+ if (!m)
153
+ continue;
154
+ const partial = handler.parse(m, line);
155
+ if (!partial)
156
+ continue;
157
+ events.push({
158
+ timestamp_iso: ts !== null ? new Date(ts).toISOString() : new Date().toISOString(),
159
+ raw_line: line.trim(),
160
+ ...partial,
161
+ });
162
+ break; // one match per line
163
+ }
164
+ }
165
+ return events;
166
+ }
167
+ /**
168
+ * Extract a unix-ms timestamp from a dmesg line. Two shapes:
169
+ * ISO: "2026-05-19T12:34:56,789012+00:00 ..." (--time-format=iso)
170
+ * ctime: "[Mon May 19 12:34:56 2026] ..." (--ctime)
171
+ *
172
+ * Relative-time format ("[12345.678]") returns null (no absolute
173
+ * anchor available without `uptime`).
174
+ */
175
+ export function parseDmesgTimestamp(line) {
176
+ const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[,.]\d+)?(?:[+-]\d{2}:?\d{2}|Z)?)/);
177
+ if (isoMatch) {
178
+ // Normalise the comma fractional separator to dot.
179
+ const iso = isoMatch[1].replace(",", ".");
180
+ const t = Date.parse(iso);
181
+ return Number.isFinite(t) ? t : null;
182
+ }
183
+ const ctimeMatch = line.match(/^\[([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\]/);
184
+ if (ctimeMatch) {
185
+ const t = Date.parse(ctimeMatch[1]);
186
+ return Number.isFinite(t) ? t : null;
187
+ }
188
+ return null;
189
+ }
190
+ export const __test_only = {
191
+ parseDmesgOutput,
192
+ parseDmesgTimestamp,
193
+ HANDLERS,
194
+ WINDOW_SECONDS,
195
+ };
196
+ //# sourceMappingURL=dmesg-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dmesg-events.js","sourceRoot":"","sources":["../../src/collect/dmesg-events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export interface EthtoolInterface {
2
+ iface: string;
3
+ advertised_auto_negotiation: boolean | null;
4
+ advertised_link_modes: string[];
5
+ }
6
+ export interface EthtoolSnapshot {
7
+ available: boolean;
8
+ reason?: string;
9
+ interfaces: EthtoolInterface[];
10
+ }
11
+ export declare function collectEthtool(): Promise<EthtoolSnapshot>;
12
+ /**
13
+ * Parse the relevant subset of `ethtool <iface>` output.
14
+ *
15
+ * The two fields of interest:
16
+ *
17
+ * Advertised auto-negotiation: Yes|No
18
+ * Advertised link modes: 10baseT/Half 10baseT/Full
19
+ * 100baseT/Half 100baseT/Full
20
+ * 1000baseT/Full
21
+ *
22
+ * Advertised link modes can span multiple lines (continuation lines
23
+ * are indented). We collect everything from the colon to the next
24
+ * non-continuation line.
25
+ */
26
+ export declare function parseEthtoolOutput(iface: string, raw: string): EthtoolInterface;
27
+ export declare const __test_only: {
28
+ parseEthtoolOutput: typeof parseEthtoolOutput;
29
+ };
@@ -0,0 +1,99 @@
1
+ // ethtool advertised link-mode collection.
2
+ //
3
+ // network.ts collects current speed via /sys/class/net/<iface>/speed.
4
+ // C15 (2026-05-19) adds the advertised modes from `ethtool <iface>` so
5
+ // Dashboard's link_speed_mismatch rule can compare current vs highest
6
+ // advertised. A 1 Gb/s link on a 10 Gb/s NIC isn't always wrong (the
7
+ // switch port might also be 1 Gb/s) but is worth surfacing.
8
+ //
9
+ // Per CC_SPEC_CRUCIBLE_C11_C18_FULL_BUNDLE_2026-05-19.md §1.3.
10
+ //
11
+ // Capability gating: `ethtool` missing (some containers lack it) ->
12
+ // available: false. Per-interface read failures are tolerated; the
13
+ // snapshot ships data for whatever interfaces returned cleanly.
14
+ import { readdirSync } from "fs";
15
+ import { run } from "../lib/exec.js";
16
+ const VIRTUAL_PREFIXES = ["lo", "veth", "docker", "br-", "virbr"];
17
+ function listPhysicalInterfaces() {
18
+ try {
19
+ const all = readdirSync("/sys/class/net");
20
+ return all.filter((iface) => iface !== "lo" &&
21
+ !VIRTUAL_PREFIXES.some((p) => iface.startsWith(p)));
22
+ }
23
+ catch {
24
+ return [];
25
+ }
26
+ }
27
+ export async function collectEthtool() {
28
+ const probe = await run("ethtool", ["--version"]);
29
+ if (!probe) {
30
+ return {
31
+ available: false,
32
+ reason: "ethtool not installed",
33
+ interfaces: [],
34
+ };
35
+ }
36
+ const interfaces = listPhysicalInterfaces();
37
+ if (interfaces.length === 0) {
38
+ return { available: true, interfaces: [] };
39
+ }
40
+ const results = [];
41
+ for (const iface of interfaces) {
42
+ const out = await run("ethtool", [iface]);
43
+ if (!out)
44
+ continue; // per-interface read failure tolerated
45
+ results.push(parseEthtoolOutput(iface, out));
46
+ }
47
+ return { available: true, interfaces: results };
48
+ }
49
+ /**
50
+ * Parse the relevant subset of `ethtool <iface>` output.
51
+ *
52
+ * The two fields of interest:
53
+ *
54
+ * Advertised auto-negotiation: Yes|No
55
+ * Advertised link modes: 10baseT/Half 10baseT/Full
56
+ * 100baseT/Half 100baseT/Full
57
+ * 1000baseT/Full
58
+ *
59
+ * Advertised link modes can span multiple lines (continuation lines
60
+ * are indented). We collect everything from the colon to the next
61
+ * non-continuation line.
62
+ */
63
+ export function parseEthtoolOutput(iface, raw) {
64
+ const lines = raw.split("\n");
65
+ let auto = null;
66
+ const modes = [];
67
+ for (let i = 0; i < lines.length; i++) {
68
+ const line = lines[i];
69
+ if (/^\s*Advertised auto-negotiation:/i.test(line)) {
70
+ const val = line.split(":").slice(1).join(":").trim();
71
+ auto = val.toLowerCase() === "yes";
72
+ }
73
+ else if (/^\s*Advertised link modes:/i.test(line)) {
74
+ // Continuation lines are indented and have no colon at the
75
+ // expected key position. Walk forward until we hit a line that
76
+ // looks like a new key (Word: at start).
77
+ let buf = line.split(":").slice(1).join(":").trim();
78
+ for (let j = i + 1; j < lines.length; j++) {
79
+ const cont = lines[j];
80
+ if (/^\s*[A-Za-z][^:]*:/.test(cont) && !/^\s/.test(cont))
81
+ break;
82
+ if (!/^\s+\S/.test(cont))
83
+ break;
84
+ buf += " " + cont.trim();
85
+ }
86
+ for (const token of buf.split(/\s+/)) {
87
+ if (token.length > 0)
88
+ modes.push(token);
89
+ }
90
+ }
91
+ }
92
+ return {
93
+ iface,
94
+ advertised_auto_negotiation: auto,
95
+ advertised_link_modes: modes,
96
+ };
97
+ }
98
+ export const __test_only = { parseEthtoolOutput };
99
+ //# sourceMappingURL=ethtool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ethtool.js","sourceRoot":"","sources":["../../src/collect/ethtool.ts"],"names":[],"mappings":"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"}
@@ -4,4 +4,50 @@ export interface FileDescriptorData {
4
4
  max: number;
5
5
  percent: number;
6
6
  }
7
+ export interface ProcessFdEntry {
8
+ pid: number;
9
+ comm: string;
10
+ fd_count: number;
11
+ rlimit_nofile_soft: number;
12
+ rlimit_nofile_hard: number;
13
+ percent_of_soft_limit: number;
14
+ }
15
+ export interface ProcessFdSnapshot {
16
+ available: boolean;
17
+ reason?: string;
18
+ top_consumers: ProcessFdEntry[];
19
+ total_processes_scanned: number;
20
+ highest_percent_of_limit: number | null;
21
+ }
7
22
  export declare function collectFileDescriptors(): FileDescriptorData;
23
+ /**
24
+ * Per-process FD scan. Walks /proc, counts FDs per PID, then reads
25
+ * `/proc/<pid>/limits` for the top-N consumers to compute proximity
26
+ * to each process's RLIMIT_NOFILE soft limit.
27
+ *
28
+ * Returns `{ available: false }` when /proc/1 is not a directory
29
+ * (essentially never on Linux; defensive).
30
+ */
31
+ export declare function collectProcessFd(): ProcessFdSnapshot;
32
+ /**
33
+ * Parse the "Max open files" line from /proc/<pid>/limits.
34
+ *
35
+ * Format (fixed-width, header + rows):
36
+ * Limit Soft Limit Hard Limit Units
37
+ * Max open files 1024 4096 files
38
+ *
39
+ * Returns null when the line is missing or fields are unparseable.
40
+ * "unlimited" maps to Infinity which Number.MAX_SAFE_INTEGER would
41
+ * misrepresent; we use 0 as a sentinel meaning "no useful soft limit"
42
+ * which makes percent_of_soft_limit zero (and the dashboard rule
43
+ * treats that as a no-emission case).
44
+ */
45
+ declare function parseOpenFilesLimit(raw: string): {
46
+ soft: number;
47
+ hard: number;
48
+ } | null;
49
+ export declare const __test_only: {
50
+ parseOpenFilesLimit: typeof parseOpenFilesLimit;
51
+ TOP_N: number;
52
+ };
53
+ export {};
@@ -1,4 +1,18 @@
1
+ // File descriptor collection.
2
+ //
3
+ // Host-wide path (collectFileDescriptors): reads /proc/sys/fs/file-nr.
4
+ // Predates C7; still consumed by dashboard's host-wide fd_exhaustion path.
5
+ //
6
+ // Per-process path (collectProcessFd): reads /proc/<pid>/fd/ + /proc/<pid>/limits
7
+ // to surface processes approaching their RLIMIT_NOFILE soft limit. Per
8
+ // CC_SPEC_CRUCIBLE_C7_C10_NETWORK_PROCESS_COLLECTION_2026-05-19.md §1.
9
+ //
10
+ // Two-pass strategy: cheap readdir over /proc to count FDs per PID, then
11
+ // expensive read of limits + comm only for the top 50 consumers. Process-
12
+ // disappeared races are tolerated silently (ENOENT swallowed).
13
+ import { readdirSync, readFileSync } from "fs";
1
14
  import { readProcFile } from "../lib/parse.js";
15
+ const TOP_N = 50;
2
16
  export function collectFileDescriptors() {
3
17
  const raw = readProcFile("/proc/sys/fs/file-nr");
4
18
  if (!raw) {
@@ -17,4 +31,138 @@ export function collectFileDescriptors() {
17
31
  const percent = Math.round(((allocated / max) * 100) * 10) / 10;
18
32
  return { allocated, free: isNaN(free) ? 0 : free, max, percent };
19
33
  }
34
+ /**
35
+ * Per-process FD scan. Walks /proc, counts FDs per PID, then reads
36
+ * `/proc/<pid>/limits` for the top-N consumers to compute proximity
37
+ * to each process's RLIMIT_NOFILE soft limit.
38
+ *
39
+ * Returns `{ available: false }` when /proc/1 is not a directory
40
+ * (essentially never on Linux; defensive).
41
+ */
42
+ export function collectProcessFd() {
43
+ let pidNames;
44
+ try {
45
+ pidNames = readdirSync("/proc");
46
+ }
47
+ catch (err) {
48
+ return {
49
+ available: false,
50
+ reason: `/proc not accessible: ${errCode(err)}`,
51
+ top_consumers: [],
52
+ total_processes_scanned: 0,
53
+ highest_percent_of_limit: null,
54
+ };
55
+ }
56
+ // Cheap pass: just count FDs per numeric PID directory.
57
+ const fdCounts = [];
58
+ let scanned = 0;
59
+ for (const entry of pidNames) {
60
+ const pid = Number(entry);
61
+ if (!Number.isInteger(pid) || pid <= 0)
62
+ continue;
63
+ scanned++;
64
+ try {
65
+ const fds = readdirSync(`/proc/${pid}/fd`);
66
+ fdCounts.push({ pid, fd_count: fds.length });
67
+ }
68
+ catch {
69
+ // Process disappeared or no permission to read fd/; skip silently.
70
+ }
71
+ }
72
+ // Top-N by fd_count.
73
+ fdCounts.sort((a, b) => b.fd_count - a.fd_count);
74
+ const candidates = fdCounts.slice(0, TOP_N);
75
+ // Expensive pass: read comm + limits for the candidates.
76
+ const top_consumers = [];
77
+ for (const { pid, fd_count } of candidates) {
78
+ const comm = safeReadTrimmed(`/proc/${pid}/comm`);
79
+ if (comm === null)
80
+ continue; // Process disappeared.
81
+ const limits = safeReadTrimmed(`/proc/${pid}/limits`);
82
+ if (limits === null)
83
+ continue;
84
+ const parsed = parseOpenFilesLimit(limits);
85
+ if (!parsed)
86
+ continue; // Malformed limits; skip.
87
+ const { soft, hard } = parsed;
88
+ const percent = soft > 0
89
+ ? Math.round((fd_count / soft) * 1000) / 10
90
+ : 0;
91
+ top_consumers.push({
92
+ pid,
93
+ comm,
94
+ fd_count,
95
+ rlimit_nofile_soft: soft,
96
+ rlimit_nofile_hard: hard,
97
+ percent_of_soft_limit: percent,
98
+ });
99
+ }
100
+ const highest = top_consumers.length > 0
101
+ ? top_consumers.reduce((m, e) => (e.percent_of_soft_limit > m ? e.percent_of_soft_limit : m), 0)
102
+ : null;
103
+ return {
104
+ available: true,
105
+ top_consumers,
106
+ total_processes_scanned: scanned,
107
+ highest_percent_of_limit: highest,
108
+ };
109
+ }
110
+ function safeReadTrimmed(path) {
111
+ try {
112
+ return readFileSync(path, "utf-8").trim();
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ function errCode(err) {
119
+ if (err && typeof err === "object" && "code" in err) {
120
+ const code = err.code;
121
+ if (typeof code === "string")
122
+ return code;
123
+ }
124
+ return "unknown";
125
+ }
126
+ /**
127
+ * Parse the "Max open files" line from /proc/<pid>/limits.
128
+ *
129
+ * Format (fixed-width, header + rows):
130
+ * Limit Soft Limit Hard Limit Units
131
+ * Max open files 1024 4096 files
132
+ *
133
+ * Returns null when the line is missing or fields are unparseable.
134
+ * "unlimited" maps to Infinity which Number.MAX_SAFE_INTEGER would
135
+ * misrepresent; we use 0 as a sentinel meaning "no useful soft limit"
136
+ * which makes percent_of_soft_limit zero (and the dashboard rule
137
+ * treats that as a no-emission case).
138
+ */
139
+ function parseOpenFilesLimit(raw) {
140
+ for (const line of raw.split("\n")) {
141
+ if (!line.startsWith("Max open files"))
142
+ continue;
143
+ // After the label there are at least two whitespace-separated values.
144
+ const rest = line.slice("Max open files".length).trim();
145
+ const parts = rest.split(/\s+/);
146
+ if (parts.length < 2)
147
+ return null;
148
+ const soft = parseLimitValue(parts[0]);
149
+ const hard = parseLimitValue(parts[1]);
150
+ if (soft === null || hard === null)
151
+ return null;
152
+ return { soft, hard };
153
+ }
154
+ return null;
155
+ }
156
+ function parseLimitValue(v) {
157
+ if (v === "unlimited")
158
+ return 0; // sentinel; see parseOpenFilesLimit comment
159
+ const n = Number(v);
160
+ if (!Number.isFinite(n) || n < 0)
161
+ return null;
162
+ return n;
163
+ }
164
+ export const __test_only = {
165
+ parseOpenFilesLimit,
166
+ TOP_N,
167
+ };
20
168
  //# sourceMappingURL=fd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fd.js","sourceRoot":"","sources":["../../src/collect/fd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAS/C,MAAM,UAAU,sBAAsB;IACpC,MAAM,GAAG,GAAG,YAAY,CAAC,sBAAsB,CAAC,CAAC;IACjD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACvD,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACvD,CAAC;IAED,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEnC,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACvD,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;IAChE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AACnE,CAAC"}
1
+ {"version":3,"file":"fd.js","sourceRoot":"","sources":["../../src/collect/fd.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,17 @@
1
- import type { IpmiInfo, FanStatus, Vendor, IpmiCapability } from "../lib/types.js";
1
+ import type { BmcVendor, IpmiInfo, FanStatus, Vendor, IpmiCapability } from "../lib/types.js";
2
+ /**
3
+ * C11 (2026-05-19): map DMI vendor to BMC vendor + parser quality tier.
4
+ *
5
+ * Fleet-tested: dell, hpe, supermicro (years of fleet validation; SEL
6
+ * classifier confirmed against real-host event streams).
7
+ * Stub: lenovo, cisco, openbmc (parser ships but unvalidated against
8
+ * real fleet data; first real customer per vendor surfaces gaps).
9
+ * Unknown: everything else (asrockrack, inspur, generic, virtual, ...);
10
+ * classifier still runs (it's keyword-driven, vendor-agnostic) but
11
+ * we tag the events honestly.
12
+ */
13
+ declare function mapVendorToBmcVendor(vendor: Vendor): BmcVendor;
14
+ declare function parserQualityFor(bmcVendor: BmcVendor): "fleet-tested" | "stub" | "unknown";
2
15
  /**
3
16
  * Collect IPMI snapshot.
4
17
  *
@@ -10,6 +23,10 @@ import type { IpmiInfo, FanStatus, Vendor, IpmiCapability } from "../lib/types.j
10
23
  * and back-compat).
11
24
  */
12
25
  export declare function collectIpmi(vendor?: Vendor, capability?: IpmiCapability): Promise<IpmiInfo>;
26
+ export declare const __test_only_c11: {
27
+ mapVendorToBmcVendor: typeof mapVendorToBmcVendor;
28
+ parserQualityFor: typeof parserQualityFor;
29
+ };
13
30
  /**
14
31
  * Re-parse the full SEL elist counting ECC events on the Memory entity.
15
32
  * Used for vendors (notably Dell) that don't expose ECC counters as
@@ -29,3 +46,4 @@ export declare function parseSelTimestamp(date: string, time: string): string;
29
46
  export declare function classifySensor(sensor: string): string;
30
47
  export declare function deriveSelSeverity(event: string, sensorType: string): string;
31
48
  export declare function parseFanStatus(output: string): FanStatus[];
49
+ export {};
@@ -1,4 +1,34 @@
1
1
  import { run } from "../lib/exec.js";
2
+ /**
3
+ * C11 (2026-05-19): map DMI vendor to BMC vendor + parser quality tier.
4
+ *
5
+ * Fleet-tested: dell, hpe, supermicro (years of fleet validation; SEL
6
+ * classifier confirmed against real-host event streams).
7
+ * Stub: lenovo, cisco, openbmc (parser ships but unvalidated against
8
+ * real fleet data; first real customer per vendor surfaces gaps).
9
+ * Unknown: everything else (asrockrack, inspur, generic, virtual, ...);
10
+ * classifier still runs (it's keyword-driven, vendor-agnostic) but
11
+ * we tag the events honestly.
12
+ */
13
+ function mapVendorToBmcVendor(vendor) {
14
+ if (vendor === "dell" || vendor === "hpe" || vendor === "supermicro")
15
+ return vendor;
16
+ if (vendor === "lenovo" || vendor === "cisco")
17
+ return vendor;
18
+ // OpenBMC isn't a DMI vendor; it would surface as "generic" with a
19
+ // BMC manufacturer of "OpenBMC Project" in ipmitool mc info. We'd
20
+ // need to probe that to identify; deferred to a follow-up.
21
+ return "unknown";
22
+ }
23
+ function parserQualityFor(bmcVendor) {
24
+ if (bmcVendor === "dell" || bmcVendor === "hpe" || bmcVendor === "supermicro") {
25
+ return "fleet-tested";
26
+ }
27
+ if (bmcVendor === "lenovo" || bmcVendor === "cisco" || bmcVendor === "openbmc") {
28
+ return "stub";
29
+ }
30
+ return "unknown";
31
+ }
2
32
  import { isPsuRedundancySensor, classifyPsuRedundancyState } from "../lib/vendor-sensors.js";
3
33
  import { filterRedundantCpuDtsSensors } from "../lib/ipmi-sensor-filter.js";
4
34
  /**
@@ -83,8 +113,13 @@ export async function collectIpmi(vendor = "generic", capability) {
83
113
  if (match)
84
114
  selCount = parseInt(match[1], 10);
85
115
  }
86
- // SEL recent events
87
- const selEvents = await collectSelEvents();
116
+ // SEL recent events. C11 (2026-05-19): tag each event with the BMC
117
+ // vendor's parser_quality so Dashboard can render an honesty surface
118
+ // for stub-parser BMCs (Lenovo, Cisco, OpenBMC).
119
+ const bmcVendor = mapVendorToBmcVendor(vendor);
120
+ const selParserQuality = parserQualityFor(bmcVendor);
121
+ const selEventsRaw = await collectSelEvents();
122
+ const selEvents = selEventsRaw.map((e) => ({ ...e, parser_quality: selParserQuality }));
88
123
  // ECC errors from SEL events (Dell iDRAC reports memory ECC only via SEL).
89
124
  // Counts ALL events since last SEL clear, not just the recent window —
90
125
  // re-parse the full SEL elist for accurate cumulative counts.
@@ -104,6 +139,7 @@ export async function collectIpmi(vendor = "generic", capability) {
104
139
  const fans = await collectFanStatus();
105
140
  return {
106
141
  available: true,
142
+ bmc_vendor: bmcVendor,
107
143
  sensors,
108
144
  ecc_errors: { correctable, uncorrectable },
109
145
  ecc_errors_from_sel: selEccCounts,
@@ -114,6 +150,7 @@ export async function collectIpmi(vendor = "generic", capability) {
114
150
  detection: capability,
115
151
  };
116
152
  }
153
+ export const __test_only_c11 = { mapVendorToBmcVendor, parserQualityFor };
117
154
  /**
118
155
  * Re-parse the full SEL elist counting ECC events on the Memory entity.
119
156
  * Used for vendors (notably Dell) that don't expose ECC counters as