mirror of
https://github.com/lukaszraczylo/claude-adam.git
synced 2026-06-05 22:49:28 +00:00
012c40b9ab
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).
93 lines
3.2 KiB
JavaScript
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;
|
|
}
|