@glassmkr/crucible 0.10.2 → 0.10.4

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,160 @@
1
+ // Tests for C1-C6 collectors (v0.10.4, 2026-05-19).
2
+ //
3
+ // Most collectors hit real filesystem paths (/sys/devices/system/edac/,
4
+ // /proc/pressure/, /proc/vmstat, /var/crash/, etc.) so we can't fully
5
+ // integration-test them outside a Linux host with the expected
6
+ // capabilities. These tests cover the pure parser functions and the
7
+ // capability-gate behavior (collectors return null on hosts where the
8
+ // surface is absent).
9
+ import { describe, it, expect, beforeEach } from "vitest";
10
+ import { __test_only as psiTest } from "../psi.js";
11
+ import { __test_only as vmstatTest, collectVmstat } from "../vmstat.js";
12
+ import { parseZpoolStatus } from "../zfs.js";
13
+ describe("C2 PSI: parsePsiLine", () => {
14
+ it("parses a complete PSI line", () => {
15
+ const r = psiTest.parsePsiLine("some avg10=0.06 avg60=0.04 avg300=0.05 total=12345");
16
+ expect(r).toEqual({ avg10: 0.06, avg60: 0.04, avg300: 0.05, total: 12345 });
17
+ });
18
+ it("returns null on malformed input", () => {
19
+ expect(psiTest.parsePsiLine("some avg10=0.06")).toBeNull();
20
+ expect(psiTest.parsePsiLine("garbage")).toBeNull();
21
+ });
22
+ });
23
+ describe("C2 PSI: parsePsiFile", () => {
24
+ it("captures both some and full lines (memory/io shape)", () => {
25
+ const out = psiTest.parsePsiFile("some avg10=1.0 avg60=2.0 avg300=3.0 total=100\nfull avg10=0.5 avg60=1.0 avg300=1.5 total=50\n");
26
+ expect(out?.some.avg10).toBe(1.0);
27
+ expect(out?.full?.avg60).toBe(1.0);
28
+ });
29
+ it("works when only 'some' is present (cpu shape)", () => {
30
+ const out = psiTest.parsePsiFile("some avg10=0.0 avg60=0.0 avg300=0.0 total=0\n");
31
+ expect(out?.some.avg10).toBe(0.0);
32
+ expect(out?.full).toBeUndefined();
33
+ });
34
+ it("returns null on empty input", () => {
35
+ expect(psiTest.parsePsiFile("")).toBeNull();
36
+ });
37
+ });
38
+ describe("C3 vmstat: collectVmstat delta tracking", () => {
39
+ // Note: vmstat reads real /proc/vmstat on the test runner; on a
40
+ // dev macOS host the file doesn't exist so collectVmstat returns
41
+ // null cleanly. This is sufficient evidence for the capability
42
+ // gate; behavioral delta tests live in the e2e tier.
43
+ beforeEach(() => {
44
+ vmstatTest.resetForTests();
45
+ });
46
+ it("returns null on a host without /proc/vmstat", () => {
47
+ // On macOS / non-Linux, readProcFile returns null and the
48
+ // collector returns null cleanly.
49
+ const r = collectVmstat();
50
+ // We accept either: null (non-Linux) or a snapshot with null
51
+ // rates (Linux but first call). Both prove the capability gate
52
+ // shape.
53
+ if (r !== null) {
54
+ expect(r.pswpin_rate).toBeNull();
55
+ expect(r.pswpout_rate).toBeNull();
56
+ }
57
+ });
58
+ });
59
+ describe("C6 ZFS: parseZpoolStatus extended vdev fields", () => {
60
+ it("parses a raidz2 pool with a mirrored SLOG and L2ARC", () => {
61
+ const status = [
62
+ " pool: tank",
63
+ " state: ONLINE",
64
+ " scan: scrub repaired 0B in 00:00:30 with 0 errors on Sun Oct 1 02:30:00 2026",
65
+ "config:",
66
+ "",
67
+ "\tNAME STATE READ WRITE CKSUM",
68
+ "\ttank ONLINE 0 0 0",
69
+ "\t raidz2-0 ONLINE 0 0 0",
70
+ "\t sda ONLINE 0 0 0",
71
+ "\t sdb ONLINE 0 0 0",
72
+ "\t sdc ONLINE 0 0 0",
73
+ "\t sdd ONLINE 0 0 0",
74
+ "logs",
75
+ "\t mirror-1 ONLINE 0 0 0",
76
+ "\t nvme0n1p2 ONLINE 0 0 0",
77
+ "\t nvme1n1p2 ONLINE 0 0 0",
78
+ "cache",
79
+ "\t nvme0n1p3 ONLINE 0 0 0",
80
+ "",
81
+ "errors: No known data errors",
82
+ ].join("\n");
83
+ const pools = parseZpoolStatus(status);
84
+ expect(pools).toHaveLength(1);
85
+ const p = pools[0];
86
+ expect(p.name).toBe("tank");
87
+ expect(p.state).toBe("ONLINE");
88
+ expect(p.vdevs).toHaveLength(1);
89
+ expect(p.vdevs[0].name).toBe("raidz2-0");
90
+ expect(p.vdevs[0].redundancy_class).toBe("raidz2");
91
+ expect(p.vdevs[0].degraded_disks_count).toBe(0);
92
+ expect(p.slog_vdevs).toHaveLength(1);
93
+ expect(p.slog_vdevs[0].name).toBe("mirror-1");
94
+ expect(p.l2arc_vdevs).toHaveLength(1);
95
+ expect(p.l2arc_vdevs[0].name).toBe("nvme0n1p3");
96
+ });
97
+ it("classifies raidz1 / raidz3 / mirror correctly", () => {
98
+ const status = [
99
+ " pool: a",
100
+ " state: ONLINE",
101
+ "config:",
102
+ "\tNAME STATE",
103
+ "\ta ONLINE",
104
+ "\t raidz1-0 ONLINE",
105
+ "\t s1 ONLINE",
106
+ "\t s2 ONLINE",
107
+ " pool: b",
108
+ " state: ONLINE",
109
+ "config:",
110
+ "\tNAME STATE",
111
+ "\tb ONLINE",
112
+ "\t raidz3-0 ONLINE",
113
+ "\t s1 ONLINE",
114
+ " pool: c",
115
+ " state: ONLINE",
116
+ "config:",
117
+ "\tNAME STATE",
118
+ "\tc ONLINE",
119
+ "\t mirror-0 ONLINE",
120
+ "\t s1 ONLINE",
121
+ ].join("\n");
122
+ const pools = parseZpoolStatus(status);
123
+ expect(pools.map((p) => p.vdevs[0]?.redundancy_class)).toEqual([
124
+ "raidz1",
125
+ "raidz3",
126
+ "mirror",
127
+ ]);
128
+ });
129
+ it("counts degraded child devices on a DEGRADED vdev", () => {
130
+ const status = [
131
+ " pool: tank",
132
+ " state: DEGRADED",
133
+ "config:",
134
+ "\tNAME STATE",
135
+ "\ttank DEGRADED",
136
+ "\t raidz2-0 DEGRADED",
137
+ "\t sda ONLINE",
138
+ "\t sdb FAULTED",
139
+ "\t sdc ONLINE",
140
+ "\t sdd ONLINE",
141
+ "",
142
+ "errors: No known data errors",
143
+ ].join("\n");
144
+ const pools = parseZpoolStatus(status);
145
+ expect(pools[0].vdevs[0].degraded_disks_count).toBe(1);
146
+ });
147
+ it("treats a single-device top-level vdev as stripe (no redundancy)", () => {
148
+ const status = [
149
+ " pool: scratch",
150
+ " state: ONLINE",
151
+ "config:",
152
+ "\tNAME STATE",
153
+ "\tscratch ONLINE",
154
+ "\t sda ONLINE",
155
+ ].join("\n");
156
+ const pools = parseZpoolStatus(status);
157
+ expect(pools[0].vdevs[0].redundancy_class).toBe("stripe");
158
+ });
159
+ });
160
+ //# sourceMappingURL=c1-c6.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"c1-c6.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/c1-c6.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { EdacSnapshot } from "../lib/types.js";
2
+ export declare function collectEdac(): EdacSnapshot | null;
@@ -0,0 +1,104 @@
1
+ // EDAC (Error Detection and Correction) counter collection.
2
+ //
3
+ // Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C1).
4
+ //
5
+ // Kernel surface: /sys/devices/system/edac/mc/mcN/{ce_count,ue_count}
6
+ // ce_count = correctable errors (memory controller saw and fixed)
7
+ // ue_count = uncorrectable errors (memory error couldn't be fixed;
8
+ // kernel may have killed the process or panicked)
9
+ //
10
+ // Per-DIMM granularity: /sys/devices/system/edac/mc/mcN/dimmM/
11
+ // dimm_ce_count, dimm_ue_count, dimm_label, dimm_location, size
12
+ //
13
+ // Capability gate: if no `mcN` directories exist under
14
+ // /sys/devices/system/edac/mc/, EDAC is not loaded (no memory
15
+ // controller driver compiled in or no DIMMs reporting) and this
16
+ // collector returns null. The dashboard ecc_errors evaluator falls
17
+ // back to its IPMI-SEL path.
18
+ //
19
+ // On hosts where EDAC is loaded but counters stay at zero, this
20
+ // collector emits zero counters — that's the correct baseline.
21
+ import { readdirSync, readFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ const EDAC_ROOT = "/sys/devices/system/edac/mc";
24
+ function readUint(path) {
25
+ try {
26
+ const s = readFileSync(path, "utf8").trim();
27
+ if (!/^\d+$/.test(s))
28
+ return null;
29
+ return parseInt(s, 10);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function readString(path) {
36
+ try {
37
+ return readFileSync(path, "utf8").trim();
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function listMcDirs() {
44
+ try {
45
+ return readdirSync(EDAC_ROOT)
46
+ .filter((n) => /^mc\d+$/.test(n))
47
+ .map((n) => join(EDAC_ROOT, n));
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ function listDimmDirs(mcPath) {
54
+ try {
55
+ return readdirSync(mcPath)
56
+ .filter((n) => /^dimm\d+$/.test(n) || /^rank\d+$/.test(n))
57
+ .map((n) => join(mcPath, n));
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ function collectDimm(dimmPath) {
64
+ const ce = readUint(join(dimmPath, "dimm_ce_count"));
65
+ const ue = readUint(join(dimmPath, "dimm_ue_count"));
66
+ if (ce === null && ue === null)
67
+ return null;
68
+ const label = readString(join(dimmPath, "dimm_label"));
69
+ const location = readString(join(dimmPath, "dimm_location"));
70
+ const sizeStr = readString(join(dimmPath, "size"));
71
+ const sizeMb = sizeStr && /^\d+$/.test(sizeStr) ? parseInt(sizeStr, 10) : null;
72
+ return {
73
+ label: label ?? "",
74
+ location: location ?? "",
75
+ size_mb: sizeMb,
76
+ ce_count: ce ?? 0,
77
+ ue_count: ue ?? 0,
78
+ };
79
+ }
80
+ export function collectEdac() {
81
+ const mcDirs = listMcDirs();
82
+ if (mcDirs.length === 0)
83
+ return null;
84
+ let totalCe = 0;
85
+ let totalUe = 0;
86
+ const dimms = [];
87
+ for (const mc of mcDirs) {
88
+ const ce = readUint(join(mc, "ce_count")) ?? 0;
89
+ const ue = readUint(join(mc, "ue_count")) ?? 0;
90
+ totalCe += ce;
91
+ totalUe += ue;
92
+ for (const dimm of listDimmDirs(mc)) {
93
+ const d = collectDimm(dimm);
94
+ if (d)
95
+ dimms.push(d);
96
+ }
97
+ }
98
+ return {
99
+ edac_corrected_total: totalCe,
100
+ edac_uncorrected_total: totalUe,
101
+ dimms,
102
+ };
103
+ }
104
+ //# sourceMappingURL=edac.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edac.js","sourceRoot":"","sources":["../../src/collect/edac.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { HardwareRaidSnapshot } from "../lib/types.js";
2
+ export declare function collectHardwareRaid(): Promise<HardwareRaidSnapshot | null>;
@@ -0,0 +1,152 @@
1
+ // Hardware RAID controller scraping.
2
+ //
3
+ // Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C5).
4
+ //
5
+ // Detects four vendor CLIs:
6
+ // - perccli (Dell PERC)
7
+ // - ssacli (HPE Smart Array)
8
+ // - storcli (LSI / Broadcom MegaRAID)
9
+ // - arcconf (Adaptec)
10
+ //
11
+ // For each installed CLI, queries the controller state and returns
12
+ // a normalized HardwareRaidController. The vendor-specific output
13
+ // formats vary considerably; this module's parsers are intentionally
14
+ // conservative — they extract a state string ("Optimal", "Degraded",
15
+ // etc.) plus an optional degraded_disks counter when the vendor output
16
+ // makes it easy to find. The dashboard's raid_degraded evaluator pages
17
+ // on any state != "Optimal"; that's the contract.
18
+ //
19
+ // Implementation scope (2026-05-19): perccli + storcli parsers are
20
+ // best-effort because the validation fleet has no hardware RAID
21
+ // controllers to verify against. Real parsing precision lands in
22
+ // follow-up PRs as customers with each vendor surface. The framework
23
+ // here ensures:
24
+ // - empty controllers[] on hosts without any vendor CLI (capability
25
+ // gate; dashboard rule no-ops),
26
+ // - empty controllers[] on hosts with the CLI but no controllers
27
+ // present (rare configurations).
28
+ //
29
+ // The dashboard's mdadm path is unaffected by this module.
30
+ import { run } from "../lib/exec.js";
31
+ async function hasCli(name) {
32
+ const out = await run("which", [name], 2000);
33
+ return !!(out && out.trim());
34
+ }
35
+ async function scrapePerccli() {
36
+ // perccli /c0 show all J — JSON output for controller 0.
37
+ // Multi-controller hosts are rare; query c0 only and let follow-ups
38
+ // expand if a customer surfaces multi-controller hardware.
39
+ const raw = await run("perccli", ["/c0", "show", "all", "J"], 10000);
40
+ if (!raw)
41
+ return [];
42
+ try {
43
+ const obj = JSON.parse(raw);
44
+ const ctrlList = obj?.Controllers ?? [];
45
+ return ctrlList.map((c) => ({
46
+ vendor: "dell",
47
+ controller_id: String(c?.["Command Status"]?.Controller ?? "0"),
48
+ state: String(c?.["Response Data"]?.["Status"]?.["Controller Status"] ?? "Unknown"),
49
+ degraded_disks: null,
50
+ raw_summary: null,
51
+ }));
52
+ }
53
+ catch {
54
+ // Output wasn't JSON (older perccli, or controller missing).
55
+ return [];
56
+ }
57
+ }
58
+ async function scrapeStorcli() {
59
+ const raw = await run("storcli", ["/call", "show", "all", "J"], 10000);
60
+ if (!raw)
61
+ return [];
62
+ try {
63
+ const obj = JSON.parse(raw);
64
+ const ctrlList = obj?.Controllers ?? [];
65
+ return ctrlList.map((c) => ({
66
+ vendor: "lsi",
67
+ controller_id: String(c?.["Command Status"]?.Controller ?? "?"),
68
+ state: String(c?.["Response Data"]?.["Status"]?.["Controller Status"] ?? "Unknown"),
69
+ degraded_disks: null,
70
+ raw_summary: null,
71
+ }));
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ }
77
+ async function scrapeSsacli() {
78
+ // ssacli ctrl all show — text format. Conservative: extract one
79
+ // line per "in slot X" entry; status reported on a "Controller Status"
80
+ // line. Real parsing lands when an HPE customer surfaces.
81
+ const raw = await run("ssacli", ["ctrl", "all", "show", "status"], 10000);
82
+ if (!raw)
83
+ return [];
84
+ const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
85
+ const controllers = [];
86
+ let pending = null;
87
+ for (const line of lines) {
88
+ const slotMatch = line.match(/in Slot (\S+)/);
89
+ if (slotMatch) {
90
+ if (pending)
91
+ controllers.push(pendingToController(pending));
92
+ pending = { slot: slotMatch[1], status: "Unknown" };
93
+ continue;
94
+ }
95
+ const statusMatch = line.match(/Controller Status:\s*(.+)/);
96
+ if (statusMatch && pending)
97
+ pending.status = statusMatch[1].trim();
98
+ }
99
+ if (pending)
100
+ controllers.push(pendingToController(pending));
101
+ return controllers;
102
+ }
103
+ function pendingToController(p) {
104
+ return {
105
+ vendor: "hpe",
106
+ controller_id: p.slot,
107
+ state: p.status,
108
+ degraded_disks: null,
109
+ raw_summary: null,
110
+ };
111
+ }
112
+ async function scrapeArcconf() {
113
+ // arcconf has no JSON mode. Best-effort: detect controllers via
114
+ // `arcconf list` and surface a placeholder state. Real parser
115
+ // for Adaptec lands when a customer surfaces.
116
+ const raw = await run("arcconf", ["list"], 10000);
117
+ if (!raw)
118
+ return [];
119
+ const controllers = [];
120
+ for (const line of raw.split("\n")) {
121
+ const m = line.match(/Controller (\d+):/i);
122
+ if (m) {
123
+ controllers.push({
124
+ vendor: "adaptec",
125
+ controller_id: m[1],
126
+ state: "Unknown",
127
+ degraded_disks: null,
128
+ raw_summary: "arcconf parsing pending — surface a customer with Adaptec hardware",
129
+ });
130
+ }
131
+ }
132
+ return controllers;
133
+ }
134
+ export async function collectHardwareRaid() {
135
+ const hasPerccli = await hasCli("perccli");
136
+ const hasStorcli = await hasCli("storcli");
137
+ const hasSsacli = await hasCli("ssacli");
138
+ const hasArcconf = await hasCli("arcconf");
139
+ if (!hasPerccli && !hasStorcli && !hasSsacli && !hasArcconf)
140
+ return null;
141
+ const controllers = [];
142
+ if (hasPerccli)
143
+ controllers.push(...(await scrapePerccli()));
144
+ if (hasStorcli)
145
+ controllers.push(...(await scrapeStorcli()));
146
+ if (hasSsacli)
147
+ controllers.push(...(await scrapeSsacli()));
148
+ if (hasArcconf)
149
+ controllers.push(...(await scrapeArcconf()));
150
+ return { controllers };
151
+ }
152
+ //# sourceMappingURL=hardware-raid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hardware-raid.js","sourceRoot":"","sources":["../../src/collect/hardware-raid.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { PsiResource, PsiSnapshot } from "../lib/types.js";
2
+ declare function parsePsiLine(line: string): PsiResource | null;
3
+ interface PsiFile {
4
+ some: PsiResource;
5
+ /** Only present for memory + io; cpu has no "full" line in current kernels. */
6
+ full?: PsiResource;
7
+ }
8
+ declare function parsePsiFile(contents: string): PsiFile | null;
9
+ /**
10
+ * Collect PSI for all three resources. Returns null if /proc/pressure/
11
+ * is unavailable (older kernel or PSI disabled). Per resource: returns
12
+ * undefined if that specific file is unreadable (rare; usually all
13
+ * three present or none).
14
+ */
15
+ export declare function collectPsi(): PsiSnapshot | null;
16
+ export declare const __test_only: {
17
+ parsePsiLine: typeof parsePsiLine;
18
+ parsePsiFile: typeof parsePsiFile;
19
+ };
20
+ export {};
@@ -0,0 +1,90 @@
1
+ // PSI (Pressure Stall Information) collection.
2
+ //
3
+ // Reads /proc/pressure/{cpu,memory,io}. The kernel exposes this on
4
+ // 4.20+ when PSI is compiled in (most modern distros). Older kernels
5
+ // or those built without `CONFIG_PSI=y` return an empty result; the
6
+ // snapshot then omits the `psi` field entirely, and the dashboard
7
+ // alert evaluator's capability gates treat that as `available: false`.
8
+ //
9
+ // Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C2).
10
+ //
11
+ // File format (all three resources share the same shape):
12
+ //
13
+ // some avg10=0.06 avg60=0.04 avg300=0.05 total=12345
14
+ // full avg10=0.00 avg60=0.00 avg300=0.00 total=678
15
+ //
16
+ // "some" = % of time at least one task was stalled on the resource.
17
+ // "full" = % of time ALL non-idle tasks were stalled (only present
18
+ // for memory and io; cpu has only "some" per kernel docs).
19
+ // "total" = cumulative microseconds since boot.
20
+ import { readProcFile } from "../lib/parse.js";
21
+ function parsePsiLine(line) {
22
+ // Expect: "some avg10=N avg60=N avg300=N total=N"
23
+ const parts = line.trim().split(/\s+/);
24
+ if (parts.length < 5)
25
+ return null;
26
+ const out = {};
27
+ for (const part of parts.slice(1)) {
28
+ const [k, v] = part.split("=");
29
+ if (k === "avg10")
30
+ out.avg10 = parseFloat(v);
31
+ else if (k === "avg60")
32
+ out.avg60 = parseFloat(v);
33
+ else if (k === "avg300")
34
+ out.avg300 = parseFloat(v);
35
+ else if (k === "total")
36
+ out.total = parseInt(v, 10);
37
+ }
38
+ if (out.avg10 === undefined ||
39
+ out.avg60 === undefined ||
40
+ out.avg300 === undefined ||
41
+ out.total === undefined) {
42
+ return null;
43
+ }
44
+ return out;
45
+ }
46
+ function parsePsiFile(contents) {
47
+ let some = null;
48
+ let full = null;
49
+ for (const line of contents.split("\n")) {
50
+ if (line.startsWith("some "))
51
+ some = parsePsiLine(line);
52
+ else if (line.startsWith("full "))
53
+ full = parsePsiLine(line);
54
+ }
55
+ if (!some)
56
+ return null;
57
+ return full ? { some, full } : { some };
58
+ }
59
+ /**
60
+ * Collect PSI for all three resources. Returns null if /proc/pressure/
61
+ * is unavailable (older kernel or PSI disabled). Per resource: returns
62
+ * undefined if that specific file is unreadable (rare; usually all
63
+ * three present or none).
64
+ */
65
+ export function collectPsi() {
66
+ const cpu = readProcFile("/proc/pressure/cpu");
67
+ const memory = readProcFile("/proc/pressure/memory");
68
+ const io = readProcFile("/proc/pressure/io");
69
+ if (!cpu && !memory && !io)
70
+ return null;
71
+ const out = {};
72
+ if (cpu) {
73
+ const parsed = parsePsiFile(cpu);
74
+ if (parsed)
75
+ out.cpu = parsed;
76
+ }
77
+ if (memory) {
78
+ const parsed = parsePsiFile(memory);
79
+ if (parsed)
80
+ out.memory = parsed;
81
+ }
82
+ if (io) {
83
+ const parsed = parsePsiFile(io);
84
+ if (parsed)
85
+ out.io = parsed;
86
+ }
87
+ return Object.keys(out).length > 0 ? out : null;
88
+ }
89
+ export const __test_only = { parsePsiLine, parsePsiFile };
90
+ //# sourceMappingURL=psi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"psi.js","sourceRoot":"","sources":["../../src/collect/psi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { RebootEvidence } from "../lib/types.js";
2
+ export declare function collectRebootEvidence(): Promise<RebootEvidence>;