Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@forwardimpact/libutil

Package Overview
Dependencies
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@forwardimpact/libutil - npm Package Compare versions

Comparing version
0.1.82
to
0.1.83
+110
src/findings.js
import path from "node:path";
// Shared ESLint-style emitter for structured Finding objects. A Finding is
// `{ id, level, path?, lineNo?, message, hint? }` where `level` is "fail" or
// "warn"; everything else is optional. Both libwiki's audit and libcoaligned's
// instruction/JTBD checks emit Finding objects and render through these
// functions for consistency.
function partition(findings) {
const failures = [];
const warnings = [];
for (const f of findings) {
if (f.level === "warn") warnings.push(f);
else failures.push(f);
}
return { failures, warnings };
}
function relPath(p, cwd) {
if (!p) return "(no path)";
if (!cwd) return p;
const rel = path.relative(cwd, p);
return rel.startsWith("..") ? p : rel;
}
function groupByPath(findings) {
const groups = new Map();
for (const f of findings) {
const key = f.path ?? "(no path)";
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(f);
}
return groups;
}
function levelLabel(level) {
return level === "warn" ? "warning" : "error";
}
function widths(group) {
return {
loc: Math.max(
0,
...group.map((f) => (f.lineNo != null ? String(f.lineNo).length : 0)),
),
level: Math.max(...group.map((f) => levelLabel(f.level).length)),
msg: Math.max(...group.map((f) => f.message.length)),
};
}
function plural(n, word) {
return `${n} ${word}${n === 1 ? "" : "s"}`;
}
function renderFinding(f, w) {
const loc =
f.lineNo != null ? String(f.lineNo).padStart(w.loc) : " ".repeat(w.loc);
const level = levelLabel(f.level).padEnd(w.level);
const msg = f.message.padEnd(w.msg);
const lines = [` ${loc} ${level} ${msg} ${f.id}`];
if (f.hint) {
const pad = 2 + w.loc + 2 + w.level + 2;
lines.push(`${" ".repeat(pad)}→ ${f.hint}`);
}
return lines;
}
function renderGroup(filePath, group, cwd) {
const w = widths(group);
const lines = [relPath(filePath, cwd)];
for (const f of group) lines.push(...renderFinding(f, w));
return lines;
}
function renderTrailer(findings) {
const { failures, warnings } = partition(findings);
const symbol = failures.length === 0 ? "⚠" : "✖";
return `${symbol} ${plural(findings.length, "problem")} (${plural(failures.length, "error")}, ${plural(warnings.length, "warning")})`;
}
/** Render findings as ESLint-style grouped output with rule IDs and hints. */
export function emitFindingsText(findings, options = {}) {
if (findings.length === 0) {
const label = options.passMessage ?? "all checks passed";
return `✓ ${label}\n`;
}
const cwd = options.cwd ?? null;
const blocks = [];
for (const [filePath, group] of groupByPath(findings)) {
blocks.push(renderGroup(filePath, group, cwd).join("\n"));
}
blocks.push(renderTrailer(findings));
return `${blocks.join("\n\n")}\n`;
}
/** Render findings as a JSON document. */
export function emitFindingsJson(findings) {
const { failures, warnings } = partition(findings);
return (
JSON.stringify(
{
result: failures.length === 0 ? "pass" : "fail",
failures,
warnings,
},
null,
2,
) + "\n"
);
}
// Generic rule-execution engine, paired with `libutil/findings.js` for output.
//
// A rule is `{ id, scope, severity, when?, check, message, hint? }`:
//
// - `scope` is an opaque string. The caller supplies a `resolveScope(scopeKey,
// ctx)` function that returns the list of subjects for that scope. Subjects
// carry whatever fields the rule's `check` and `message` functions read
// (commonly `path`, `lineNo`, `text`, parsed-row fields, etc.).
// - `when(subject, ctx)` is an optional predicate — falsy skips the rule.
// - `check(subject, ctx)` returns `null` (clean), a single finding item, or
// an array of finding items. Each item is a plain object whose fields the
// rule's `message` function reads (e.g., `{ value: 572 }`).
// - `message(subject, item, ctx)` builds the human-readable message string.
// - `hint` is an optional static string rendered as an action prompt by the
// text emitter.
//
// `ctx` is passed unchanged to every rule. Cross-subject state (e.g., a
// duplicate-detection map) lives on `ctx` and is mutated by the rule during
// iteration — the engine iterates rules grouped by scope in stable order.
function groupByScope(rules) {
const groups = new Map();
for (const rule of rules) {
if (!groups.has(rule.scope)) groups.set(rule.scope, []);
groups.get(rule.scope).push(rule);
}
return groups;
}
function applyRule(rule, subject, ctx) {
if (rule.when && !rule.when(subject, ctx)) return [];
const result = rule.check(subject, ctx);
if (result == null) return [];
const items = Array.isArray(result) ? result : [result];
return items.map((item) => ({
id: rule.id,
level: rule.severity,
path: subject.path ?? null,
lineNo: item.lineNo ?? subject.lineNo ?? null,
message: rule.message(subject, item, ctx),
hint: rule.hint ?? null,
}));
}
/** Apply a declarative rule catalogue against a context with an injected scope resolver. Returns a flat Finding[]. */
export function runRules(rules, ctx, { resolveScope }) {
const findings = [];
for (const [scopeKey, scopeRules] of groupByScope(rules)) {
for (const subject of resolveScope(scopeKey, ctx)) {
for (const rule of scopeRules) {
findings.push(...applyRule(rule, subject, ctx));
}
}
}
return findings;
}
+1
-1
{
"name": "@forwardimpact/libutil",
"version": "0.1.82",
"version": "0.1.83",
"description": "Cross-cutting utilities: retry, hashing, token counting, and project discovery.",

@@ -5,0 +5,0 @@ "keywords": [

@@ -166,1 +166,3 @@ import crypto from "crypto";

export { waitFor } from "./wait.js";
export { emitFindingsText, emitFindingsJson } from "./findings.js";
export { runRules } from "./rules.js";