Files
claude-adam/adam/scripts/adam-utils.mjs
T
lukaszraczylo 012c40b9ab chore(v0.3.3): analyst observability, A/B measurement, journal hygiene
Storage/window/exclusion split (#7): ISO-week journal rotation with safety
fuse replaces size-based rotation (fixes silent under-counting when clusters
straddle boundaries). Per-signal sliding windows via adam-window.mjs guard
against stale signal accumulation. Legacy YYYY-MM-DD-<ts>.jsonl files remain
readable.

Error fingerprint normalization (#3): adam-observe.mjs extracts canonical
error codes (ENOENT, ECONNREFUSED, etc.) and normalizes paths/timestamps/hex
before hashing. 'Connection refused' and 'ECONNREFUSED' now cluster identically.

Correction corpus expansion (#1): strong tokens (stop, wrong, undo, try again,
different approach, etc.) fire on any occurrence. Weak tokens (no, actually,
wait) require negation/contrast co-occurrence within 8 tokens. Kills the
'actually, I think...' false positive.

Analyst observability (#6): mandatory clustering trace block; adam-explain.mjs
parses to summary/full/json. Cluster decisions now surface rejection reasons
(threshold, contradiction, window). Persisted to ~/.claude/adam/last-trace.txt.

Dead_end nudge proposal type (#2): single-session auto-apply gate (>=3
dead_end events). Action appends to active-nudges.json, surfaced via
adam-nudge.mjs at next SessionStart. Lower blast than skill_edit.

Per-(skill, fingerprint) cooldown (#4): adam-cooldown.mjs replaces coarse
per-skill check. proposal_fingerprint = djb2(skill_slug + cluster_id +
normalized_diff_body). Legacy applied/rejected records gate via 'legacy'
fingerprint fallback through resolveSkill helper (handles target_skill,
skill, or target: <path>).

task_completed scoring integration (#8): adam-score.mjs computes per-session
urgency dampener (3 task_completed -> 0.5) and reinforcement candidates
(skills cited in >=3 clean completions). New 'reinforcement' proposal type
appends to reinforcements.jsonl on apply (no code/memory mutation).

A/B effectiveness measurement (#5): every auto-applied edit appends to
ab-tracking.jsonl. adam-ab-measure.mjs computes 7d pre/post signal-count
delta per entry (improved / neutral / regressed / no_baseline / pending).
Analyst surfaces regressions at top of /reflect output.

Upgrade UX overhaul (#9): adam-upgrade.mjs implements --list/--diff/--accept
/--accept-all. SessionStart nudge prints pending-merge warning when
.adam-new files exist (latency ~20ms via fixed shortlist). install.sh
emits unmissable final-message hint after creating any .adam-new file.

Simplify pass: adam-utils.mjs deduplicates readJsonlSafe / listJsonlFiles /
parseFrontmatter across 8 scripts. Net -46 LOC.

Test coverage: 30 -> 87 tests. Every new feature has feature-validating
assertions (false-case coverage included). T77 statically verifies install.sh
references every adam-*.mjs source script (would have caught the missing
adam-utils inclusion that review #2 surfaced).
2026-05-13 01:02:33 +01:00

93 lines
3.2 KiB
JavaScript

// adam-utils.mjs — shared helpers used across adam-* scripts.
//
// Pure library: no shebang, not a CLI. Imported by adam-window.mjs,
// adam-score.mjs, adam-ab-measure.mjs, adam-nudge-eligibility.mjs (jsonl
// helpers) and adam-apply-reinforcement.mjs, adam-archive.mjs,
// adam-cooldown.mjs (parseFrontmatter).
//
// All helpers swallow read/parse failures by design — callers expect to keep
// going on a corrupt line/file rather than abort the whole pipeline.
import { readFileSync, readdirSync, existsSync } from "node:fs";
import { join } from "node:path";
// listJsonlFiles: list *.jsonl files in `dir`. Missing dir or read failure
// returns []. Filenames are joined with `dir` so callers can read directly.
export function listJsonlFiles(dir) {
if (!existsSync(dir)) return [];
try {
return readdirSync(dir)
.filter((n) => n.endsWith(".jsonl"))
.map((n) => join(dir, n));
} catch { return []; }
}
// readJsonlSafe: read a .jsonl file and return an array of parsed objects.
// Missing file, unreadable file, or any malformed line are silently skipped.
export function readJsonlSafe(path) {
if (!existsSync(path)) return [];
let buf;
try { buf = readFileSync(path, "utf8"); } catch { return []; }
const out = [];
for (const line of buf.split("\n")) {
if (!line) continue;
try { out.push(JSON.parse(line)); } catch { /* skip malformed */ }
}
return out;
}
// parseFrontmatter: parse a markdown YAML-ish frontmatter block into a flat
// object. Supports:
// - inline scalars key: value
// - inline arrays key: [a, b, c]
// - block-form arrays key:\n - a\n - b
// Quotes around scalar values are stripped. Comment-only lines (`# ...`) and
// keys with empty inline values that are NOT followed by a block array are
// skipped (preserves prior cooldown.mjs behavior). Missing frontmatter → {}.
export function parseFrontmatter(content) {
const m = content.match(/^---\n([\s\S]*?)\n---/);
if (!m) return {};
const out = {};
const lines = m[1].split("\n");
let i = 0;
while (i < lines.length) {
const line = lines[i];
const idx = line.indexOf(":");
if (idx === -1) { i++; continue; }
const key = line.slice(0, idx).trim();
if (!key || key.startsWith("#")) { i++; continue; }
const rawValue = line.slice(idx + 1).trim();
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
const inner = rawValue.slice(1, -1)
.split(",")
.map((s) => s.trim().replace(/^['"]|['"]$/g, ""))
.filter(Boolean);
out[key] = inner;
i++;
continue;
}
if (!rawValue) {
// Possible block-form array: look ahead for ` - item` lines.
const arr = [];
let j = i + 1;
while (j < lines.length && /^\s*-\s+/.test(lines[j])) {
const item = lines[j].replace(/^\s*-\s+/, "").trim().replace(/^['"]|['"]$/g, "");
if (item) arr.push(item);
j++;
}
if (arr.length) {
out[key] = arr;
i = j;
continue;
}
// Empty value, no block follow-up: skip (cooldown/apply-reinforcement
// expectation — empty scalars are noise).
i++;
continue;
}
out[key] = rawValue.replace(/^['"]|['"]$/g, "");
i++;
}
return out;
}