From e86fd4b42278d35fefe0ea7386c8742c57d9d560 Mon Sep 17 00:00:00 2001 From: Richard Kovacs Date: Tue, 18 Aug 2026 09:29:35 +0200 Subject: [PATCH 01/35] fix(cli): merge on import instead of overwriting the receiving home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pal cli import` called `zip.extractAllTo(home, true)`, overwriting every colliding path. Importing machine A onto machine B discarded B's side of every append-only log — the receiving machine's ratings, reflections and project history were replaced wholesale, silently and with no undo. That is data loss inside the feature that advertises portability. Import now merges by default: *.jsonl union of both sides, deduplicated by exact line new files written as-is identical no-op diverged local kept in place, incoming quarantined under backups/ denylisted never written Deduplication is required rather than optional: a zip import has no merge base, so a plain concatenation would double every record on re-import. Exact-line identity is the key — PAL's jsonl files share no schema, and every writer serializes a record the same way, so byte equality is the only key that holds across all of them. `machine.json` is denied at the import boundary. It carries an install's identity; importing it would give two machines one id and silently break every origin-scoped read built on top of it. `--overwrite` preserves restore-a-backup semantics as an explicit choice, and dry-run names which of the two modes it would take. Each run appends to memory/state/import-log.jsonl so a merged corpus stays attributable. Verified by breaking it first: removing the dedupe check fails only the idempotency test; reverting to extractAllTo fails merge, quarantine and idempotency. Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/cli/index.ts | 74 ++++++++++-- src/hooks/lib/import-merge.ts | 191 ++++++++++++++++++++++++++++++ test/export.test.ts | 12 +- test/import-merge.test.ts | 211 ++++++++++++++++++++++++++++++++++ 4 files changed, 479 insertions(+), 9 deletions(-) create mode 100644 src/hooks/lib/import-merge.ts create mode 100644 test/import-merge.test.ts diff --git a/src/cli/index.ts b/src/cli/index.ts index 25d159e..167053d 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -12,7 +12,7 @@ * uninstall [--claude] [--opencode] [--cursor] [--codex] Remove hooks/skills for targets * update Update PAL (git pull or npm update) * export [path] [--dry-run] Export user state to zip - * import [path] [--dry-run] Import user state from zip + * import [path] [--dry-run] [--overwrite] Merge user state from zip * status Show current PAL configuration * doctor Check prerequisites and system health * usage Summarize token usage and cost @@ -35,6 +35,7 @@ import { } from "node:fs"; import { homedir } from "node:os"; import { resolve } from "node:path"; +import { appendImportLog, mergeArchive, summarize } from "../hooks/lib/import-merge"; import { inference, previewInferenceRoute } from "../hooks/lib/inference"; import { DEBUG_LOG_MAX_ROTATED, logDebug } from "../hooks/lib/log"; import { palHome, palPkg, paths, platform } from "../hooks/lib/paths"; @@ -274,7 +275,7 @@ function showHelp() { pal cli uninstall [--claude] [--opencode] [--cursor] [--codex] Remove hooks for targets pal cli update Update PAL (git pull or npm update) pal cli export [path] [--dry-run] Export state to zip - pal cli import [path] [--dry-run] Import state from zip + pal cli import [path] [--dry-run] Merge state from zip (--overwrite to replace) pal cli status Show PAL configuration pal cli doctor [--probe-inference] Check prerequisites and health (--probe fires real inference per route) pal cli migrate [--list] [--dry-run] Run pending data migrations @@ -1293,6 +1294,7 @@ async function importState(args: string[]) { const home = palHome(); const dryRun = args.includes("--dry-run"); + const overwrite = args.includes("--overwrite"); const pathArg = args.find((a) => !a.startsWith("-")); logDebug("import", `start dryRun=${dryRun} pathArg=${pathArg ?? "(auto)"}`); @@ -1369,16 +1371,74 @@ async function importState(args: string[]) { process.exit(0); } - logDebug("import", `zip=${zipPath} entries=${entries.length} dryRun=${dryRun}`); + logDebug( + "import", + `zip=${zipPath} entries=${entries.length} dryRun=${dryRun} overwrite=${overwrite}` + ); if (dryRun) { - console.log(`Would import ${entries.length} files → ${home}\n`); + console.log( + `Would ${overwrite ? "overwrite with" : "merge"} ${entries.length} files → ${home}\n` + ); for (const e of entries) console.log(` ${e.entryName}`); - } else { + return; + } + + if (overwrite) { zip.extractAllTo(home, true); - logDebug("import", `done extracted=${entries.length} to=${home}`); - console.log(`Imported ${entries.length} files → ${home}`); + logDebug("import", `done overwrote=${entries.length} to=${home}`); + console.log(`Imported ${entries.length} files → ${home} (overwrite)`); + appendImportLog(home, { + ts: new Date().toISOString(), + archive: zipPath, + mode: "overwrite", + created: 0, + merged: 0, + identical: 0, + conflicts: 0, + skipped: 0, + linesAdded: 0, + quarantineDir: null, + }); log.info("Run 'pal cli install' to re-register hooks."); + return; + } + + const quarantineDir = resolve( + home, + "backups", + `import-conflicts-${new Date() + .toISOString() + .replace(/[-:T.]/g, "") + .slice(0, 14)}` + ); + const result = mergeArchive( + entries.map((e) => ({ path: e.entryName, data: () => e.getData() })), + home, + quarantineDir + ); + + appendImportLog(home, { + ts: new Date().toISOString(), + archive: zipPath, + mode: "merge", + created: result.created.length, + merged: result.merged.length, + identical: result.identical.length, + conflicts: result.conflicts.length, + skipped: result.skipped.length, + linesAdded: result.linesAdded, + quarantineDir: result.quarantineDir, + }); + + logDebug("import", `done merge ${summarize(result)} to=${home}`); + console.log(`Imported → ${home}: ${summarize(result)}`); + if (result.conflicts.length > 0) { + log.warn( + `${result.conflicts.length} file(s) diverged — local kept, incoming saved to ${result.quarantineDir}` + ); + for (const c of result.conflicts) console.log(` ${c}`); } + log.info("Run 'pal cli install' to re-register hooks."); } async function update() { diff --git a/src/hooks/lib/import-merge.ts b/src/hooks/lib/import-merge.ts new file mode 100644 index 0000000..d88b1c6 --- /dev/null +++ b/src/hooks/lib/import-merge.ts @@ -0,0 +1,191 @@ +/** + * Import merge — fold an export archive into an existing PAL home without + * destroying local records. + * + * `extractAllTo(home, true)` overwrites every colliding path, so importing + * machine A onto machine B silently discards B's side of every append-only log. + * This module replaces that with a per-type policy: + * + * *.jsonl union of both sides, deduplicated by exact line + * new files written as-is + * identical no-op + * diverged local kept in place, incoming quarantined under backups/ + * denylisted never written (machine identity, rebuildable indexes) + * + * Every policy is idempotent: re-importing the same archive is a no-op on the + * corpus. + */ + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; + +/** One file inside an export archive, decoupled from the zip library. */ +export interface ArchiveEntry { + path: string; + data(): Buffer; +} + +export interface MergeResult { + created: string[]; + merged: string[]; + identical: string[]; + conflicts: string[]; + skipped: string[]; + linesAdded: number; + quarantineDir: string | null; +} + +/** + * Paths that must never cross machines. `machine.json` carries this install's + * identity — importing it would give two machines one id and silently break + * every origin-scoped read. The retrieval index is rebuilt from its sources. + */ +const NEVER_IMPORT = ["machine.json", "memory/learning/.retrieval-index.json"]; + +function normalize(path: string): string { + return path.replaceAll("\\", "/").replace(/^\.\//, ""); +} + +export function isNeverImport(path: string): boolean { + const rel = normalize(path); + return NEVER_IMPORT.some((deny) => rel === deny || rel.endsWith(`/${deny}`)); +} + +function isJsonl(path: string): boolean { + return normalize(path).endsWith(".jsonl"); +} + +function splitLines(raw: string): string[] { + return raw.split("\n").filter((l) => l.trim().length > 0); +} + +/** + * Union of two JSONL bodies, local order preserved, incoming lines appended + * only when not already present. Exact-line identity is the dedupe key — no + * schema is shared across PAL's jsonl files, and every writer serializes a + * record the same way, so byte equality is the only key that holds for all of + * them. + */ +export function mergeJsonlLines( + localRaw: string, + incomingRaw: string +): { text: string; added: number } { + const local = splitLines(localRaw); + const seen = new Set(local); + const added: string[] = []; + for (const line of splitLines(incomingRaw)) { + if (seen.has(line)) continue; + seen.add(line); + added.push(line); + } + const all = [...local, ...added]; + return { text: all.length > 0 ? `${all.join("\n")}\n` : "", added: added.length }; +} + +function writeFileEnsuringDir(target: string, data: Buffer | string): void { + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, data); +} + +function quarantine(quarantineDir: string, rel: string, data: Buffer): void { + writeFileEnsuringDir(resolve(quarantineDir, rel), data); +} + +/** + * Merge every archive entry into `home`. `quarantineDir` receives the incoming + * copy of any file that diverged from its local counterpart, so a conflict + * loses neither side. + */ +export function mergeArchive( + entries: ArchiveEntry[], + home: string, + quarantineDir: string +): MergeResult { + const result: MergeResult = { + created: [], + merged: [], + identical: [], + conflicts: [], + skipped: [], + linesAdded: 0, + quarantineDir: null, + }; + + for (const entry of entries) { + const rel = normalize(entry.path); + if (rel.length === 0 || rel.endsWith("/")) continue; + + if (isNeverImport(rel)) { + result.skipped.push(rel); + continue; + } + + const target = resolve(home, rel); + const incoming = entry.data(); + + if (!existsSync(target)) { + writeFileEnsuringDir(target, incoming); + result.created.push(rel); + continue; + } + + const localRaw = readFileSync(target); + if (localRaw.equals(incoming)) { + result.identical.push(rel); + continue; + } + + if (isJsonl(rel)) { + const { text, added } = mergeJsonlLines( + localRaw.toString("utf-8"), + incoming.toString("utf-8") + ); + writeFileSync(target, text); + result.merged.push(rel); + result.linesAdded += added; + continue; + } + + quarantine(quarantineDir, rel, incoming); + result.conflicts.push(rel); + result.quarantineDir = quarantineDir; + } + + return result; +} + +export interface ImportLogEntry { + ts: string; + archive: string; + mode: "merge" | "overwrite"; + created: number; + merged: number; + identical: number; + conflicts: number; + skipped: number; + linesAdded: number; + quarantineDir: string | null; +} + +/** Append one record per import so a merged corpus stays attributable. */ +export function appendImportLog(home: string, entry: ImportLogEntry): void { + const logPath = resolve(home, "memory", "state", "import-log.jsonl"); + mkdirSync(dirname(logPath), { recursive: true }); + const line = `${JSON.stringify(entry)}\n`; + if (existsSync(logPath)) { + writeFileSync(logPath, readFileSync(logPath, "utf-8") + line); + return; + } + writeFileSync(logPath, line); +} + +export function summarize(result: MergeResult): string { + const parts = [ + `${result.created.length} new`, + `${result.merged.length} merged (+${result.linesAdded} records)`, + `${result.identical.length} unchanged`, + ]; + if (result.conflicts.length > 0) parts.push(`${result.conflicts.length} conflicts`); + if (result.skipped.length > 0) parts.push(`${result.skipped.length} skipped`); + return parts.join(", "); +} diff --git a/test/export.test.ts b/test/export.test.ts index 6404b0e..a334ed2 100644 --- a/test/export.test.ts +++ b/test/export.test.ts @@ -176,10 +176,18 @@ describe("cli import — folder arg", () => { const exportResult = palCli(["export", workDir]); expect(exportResult.status).toBe(0); - // Import from workDir (folder arg) — pipe "y" to the confirmation prompt + // Import from workDir (folder arg) — pipe "y" to the confirmation prompt. + // Dry-run names the mode it would take, since merge and overwrite differ + // destructively. const importResult = palCli(["import", workDir, "--dry-run"], { input: "y\n" }); expect(importResult.status).toBe(0); - expect(importResult.stdout).toContain("Would import"); + expect(importResult.stdout).toContain("Would merge"); + + const overwriteDry = palCli(["import", workDir, "--dry-run", "--overwrite"], { + input: "y\n", + }); + expect(overwriteDry.status).toBe(0); + expect(overwriteDry.stdout).toContain("Would overwrite with"); } finally { rmSync(workDir, { recursive: true, force: true }); } diff --git a/test/import-merge.test.ts b/test/import-merge.test.ts new file mode 100644 index 0000000..4490fc6 --- /dev/null +++ b/test/import-merge.test.ts @@ -0,0 +1,211 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; + +const CLI = resolve(import.meta.dir, "../src/cli/index.ts"); + +let SRC: string; +let DST: string; +let WORK: string; + +function cli(home: string, args: string[], input = "y\n") { + return spawnSync("bun", ["run", CLI, "cli", ...args], { + env: { ...process.env, PAL_HOME: home }, + encoding: "utf-8", + input, + timeout: 20000, + }); +} + +function write(home: string, rel: string, content: string) { + const p = resolve(home, rel); + mkdirSync(resolve(p, ".."), { recursive: true }); + writeFileSync(p, content); +} + +function read(home: string, rel: string): string { + return readFileSync(resolve(home, rel), "utf-8"); +} + +function lines(home: string, rel: string): string[] { + return read(home, rel) + .split("\n") + .filter((l) => l.trim()); +} + +beforeEach(() => { + SRC = mkdtempSync(resolve(tmpdir(), "pal-src-")); + DST = mkdtempSync(resolve(tmpdir(), "pal-dst-")); + WORK = mkdtempSync(resolve(tmpdir(), "pal-work-")); + + // SRC = the "Mac": two ratings, one reflection, its own skill + write( + SRC, + "memory/signals/ratings.jsonl", + '{"ts":"2026-01-01","rating":9}\n{"ts":"2026-01-02","rating":8}\n' + ); + write( + SRC, + "memory/learning/reflections/algorithm-reflections.jsonl", + '{"timestamp":"2026-01-01","q1":"mac lesson"}\n' + ); + write(SRC, "skills/mac-skill/SKILL.md", "# Mac Skill\n"); + write(SRC, "telos/GOALS.md", "# Goals\nmac version\n"); + + // DST = the "Linux box": a DIFFERENT rating, a different reflection, its own skill + write(DST, "memory/signals/ratings.jsonl", '{"ts":"2026-02-01","rating":6}\n'); + write( + DST, + "memory/learning/reflections/algorithm-reflections.jsonl", + '{"timestamp":"2026-02-01","q1":"linux lesson"}\n' + ); + write(DST, "skills/linux-skill/SKILL.md", "# Linux Skill\n"); + write(DST, "telos/GOALS.md", "# Goals\nlinux version\n"); +}); + +afterEach(() => { + for (const d of [SRC, DST, WORK]) rmSync(d, { recursive: true, force: true }); +}); + +describe("import into a NON-EMPTY home", () => { + test("merges jsonl records from both sides instead of overwriting", () => { + expect(cli(SRC, ["export", WORK]).status).toBe(0); + const r = cli(DST, ["import", WORK]); + expect(r.status).toBe(0); + + const ratings = lines(DST, "memory/signals/ratings.jsonl"); + // local record survives + expect(ratings.some((l) => l.includes("2026-02-01"))).toBe(true); + // imported records arrive + expect(ratings.some((l) => l.includes("2026-01-01"))).toBe(true); + expect(ratings.some((l) => l.includes("2026-01-02"))).toBe(true); + expect(ratings.length).toBe(3); + + const refl = lines(DST, "memory/learning/reflections/algorithm-reflections.jsonl"); + expect(refl.length).toBe(2); + expect(refl.some((l) => l.includes("linux lesson"))).toBe(true); + expect(refl.some((l) => l.includes("mac lesson"))).toBe(true); + }); + + test("keeps both machines' personal skills", () => { + expect(cli(SRC, ["export", WORK]).status).toBe(0); + expect(cli(DST, ["import", WORK]).status).toBe(0); + + expect(existsSync(resolve(DST, "skills", "linux-skill", "SKILL.md"))).toBe(true); + expect(existsSync(resolve(DST, "skills", "mac-skill", "SKILL.md"))).toBe(true); + }); + + test("does not destroy a diverged non-jsonl file; quarantines the incoming copy", () => { + expect(cli(SRC, ["export", WORK]).status).toBe(0); + expect(cli(DST, ["import", WORK]).status).toBe(0); + + // local wins in place + expect(read(DST, "telos/GOALS.md")).toContain("linux version"); + // incoming is preserved somewhere under backups/, not silently dropped + const found = spawnSync( + "sh", + ["-c", `find ${DST}/backups -name GOALS.md | head -1`], + { + encoding: "utf-8", + } + ).stdout.trim(); + expect(found.length).toBeGreaterThan(0); + expect(readFileSync(found, "utf-8")).toContain("mac version"); + }); + + test("is idempotent — importing twice does not duplicate records", () => { + expect(cli(SRC, ["export", WORK]).status).toBe(0); + expect(cli(DST, ["import", WORK]).status).toBe(0); + const afterFirst = lines(DST, "memory/signals/ratings.jsonl").length; + + expect(cli(DST, ["import", WORK]).status).toBe(0); + const afterSecond = lines(DST, "memory/signals/ratings.jsonl").length; + + expect(afterSecond).toBe(afterFirst); + expect(afterSecond).toBe(3); + }); + + test("writes an import log entry per run", () => { + expect(cli(SRC, ["export", WORK]).status).toBe(0); + expect(cli(DST, ["import", WORK]).status).toBe(0); + expect(cli(DST, ["import", WORK]).status).toBe(0); + + const logLines = lines(DST, "memory/state/import-log.jsonl"); + expect(logLines.length).toBe(2); + expect(JSON.parse(logLines[0])).toHaveProperty("archive"); + }); + + test("--overwrite restores backup semantics (local side replaced)", () => { + expect(cli(SRC, ["export", WORK]).status).toBe(0); + expect(cli(DST, ["import", WORK, "--overwrite"]).status).toBe(0); + + const ratings = lines(DST, "memory/signals/ratings.jsonl"); + expect(ratings.length).toBe(2); + expect(ratings.some((l) => l.includes("2026-02-01"))).toBe(false); + }); + + test("never imports machine.json even if present in the archive", () => { + write(SRC, "memory/state/x.json", "{}"); + writeFileSync(resolve(SRC, "machine.json"), '{"id":"SRC-ID"}'); + writeFileSync(resolve(DST, "machine.json"), '{"id":"DST-ID"}'); + expect(cli(SRC, ["export", WORK]).status).toBe(0); + expect(cli(DST, ["import", WORK]).status).toBe(0); + + expect(read(DST, "machine.json")).toContain("DST-ID"); + }); +}); + +describe("mergeJsonlLines", () => { + test("appends only unseen lines, preserving local order", async () => { + const { mergeJsonlLines } = await import("../src/hooks/lib/import-merge"); + const r = mergeJsonlLines('{"a":1}\n{"b":2}\n', '{"b":2}\n{"c":3}\n'); + expect(r.added).toBe(1); + expect(r.text).toBe('{"a":1}\n{"b":2}\n{"c":3}\n'); + }); + + test("is a no-op when incoming is a subset", async () => { + const { mergeJsonlLines } = await import("../src/hooks/lib/import-merge"); + const r = mergeJsonlLines('{"a":1}\n{"b":2}\n', '{"a":1}\n'); + expect(r.added).toBe(0); + expect(r.text).toBe('{"a":1}\n{"b":2}\n'); + }); + + test("merging its own output again adds nothing", async () => { + const { mergeJsonlLines } = await import("../src/hooks/lib/import-merge"); + const incoming = '{"c":3}\n'; + const once = mergeJsonlLines('{"a":1}\n', incoming); + const twice = mergeJsonlLines(once.text, incoming); + expect(twice.added).toBe(0); + expect(twice.text).toBe(once.text); + }); + + test("tolerates blank lines and a missing trailing newline", async () => { + const { mergeJsonlLines } = await import("../src/hooks/lib/import-merge"); + const r = mergeJsonlLines('{"a":1}', '\n\n{"b":2}'); + expect(r.text).toBe('{"a":1}\n{"b":2}\n'); + }); +}); + +describe("isNeverImport", () => { + test("blocks machine identity and rebuildable indexes", async () => { + const { isNeverImport } = await import("../src/hooks/lib/import-merge"); + expect(isNeverImport("machine.json")).toBe(true); + expect(isNeverImport("memory/learning/.retrieval-index.json")).toBe(true); + }); + + test("allows ordinary corpus files", async () => { + const { isNeverImport } = await import("../src/hooks/lib/import-merge"); + expect(isNeverImport("memory/signals/ratings.jsonl")).toBe(false); + expect(isNeverImport("telos/GOALS.md")).toBe(false); + expect(isNeverImport("skills/my-skill/SKILL.md")).toBe(false); + }); +}); From c44b9ae9028dda9439ce60530343142f44d0c865 Mon Sep 17 00:00:00 2001 From: Richard Kovacs Date: Tue, 18 Aug 2026 10:16:20 +0200 Subject: [PATCH 02/35] test: make the suite order-independent under randomized execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - run the suite with --randomize - scope the signals and token-usage homes to each test - capture the cli init and subagent link results in beforeAll - give the needsRebuild check its own agent directories - build the export zip in each test that reads it - prime the install idempotency check with its own install Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- package.json | 2 +- test/claude-md.test.ts | 10 ++++++---- test/cli.test.ts | 13 +++++++------ test/export.test.ts | 12 ++++++++---- test/install-smoke.test.ts | 3 ++- test/signals.test.ts | 4 ++-- test/subagent-link.test.ts | 6 ++++-- test/token-usage.test.ts | 6 ++++-- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 4070788..9e9dbc2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "scripts": { "type-check": "tsc --noEmit", - "test": "bun test", + "test": "bun test --randomize", "format": "biome format", "format-write": "biome format --write", "lint": "biome lint", diff --git a/test/claude-md.test.ts b/test/claude-md.test.ts index 7028391..b71d48b 100644 --- a/test/claude-md.test.ts +++ b/test/claude-md.test.ts @@ -121,10 +121,12 @@ describe("buildClaudeCodeMd", () => { describe("needsRebuild", () => { test("returns true when no AGENTS.md exists", async () => { - process.env.PAL_CLAUDE_DIR = resolve(TEST_HOME, ".claude"); - process.env.PAL_OPENCODE_DIR = resolve(TEST_HOME, ".opencode"); - mkdirSync(resolve(TEST_HOME, ".claude"), { recursive: true }); - mkdirSync(resolve(TEST_HOME, ".opencode"), { recursive: true }); + // Dirs of its own: the sibling symlink-repair test calls regenerateIfNeeded(), + // which writes an AGENTS.md into the shared .claude/.opencode pair. + process.env.PAL_CLAUDE_DIR = resolve(TEST_HOME, ".claude-empty"); + process.env.PAL_OPENCODE_DIR = resolve(TEST_HOME, ".opencode-empty"); + mkdirSync(resolve(TEST_HOME, ".claude-empty"), { recursive: true }); + mkdirSync(resolve(TEST_HOME, ".opencode-empty"), { recursive: true }); const { needsRebuild } = await import("../src/hooks/lib/claude-md"); expect(needsRebuild()).toBe(true); diff --git a/test/cli.test.ts b/test/cli.test.ts index 2800103..82e1370 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -26,10 +26,15 @@ function pal(...args: string[]) { }); } +let init: ReturnType; + +// `pal cli init` runs `bun install --frozen-lockfile` + telos scaffolding + doctor +// pre-flight; on Windows this is ~5–6s, occasionally over bun-test's 5s default. beforeAll(() => { if (existsSync(TEST_HOME)) rmSync(TEST_HOME, { recursive: true }); mkdirSync(TEST_HOME, { recursive: true }); -}); + init = pal("cli", "init"); +}, 30000); describe("pal help", () => { test("shows help text", () => { @@ -47,12 +52,8 @@ describe("pal help", () => { }); describe("pal cli init", () => { - // `pal cli init` runs `bun install --frozen-lockfile` + telos scaffolding + - // doctor pre-flight; on Windows this is ~5–6s, occasionally over bun-test's - // 5s default. Bump to match the spawnSync timeout so we test the real path. test("scaffolds telos and memory directories", () => { - const result = pal("cli", "init"); - expect(result.status).toBe(0); + expect(init.status).toBe(0); const telosDir = resolve(TEST_HOME, "telos"); expect(existsSync(telosDir)).toBe(true); diff --git a/test/export.test.ts b/test/export.test.ts index a334ed2..7c0c4c4 100644 --- a/test/export.test.ts +++ b/test/export.test.ts @@ -109,18 +109,22 @@ describe("collectExportFiles", () => { }); describe("exportZip", () => { - test("creates a valid zip file", async () => { + const zipPath = resolve(TEST_HOME, "test-export.zip"); + + async function writeZip() { const { exportZip } = await import("../src/hooks/lib/export"); - const zipPath = resolve(TEST_HOME, "test-export.zip"); + return exportZip(zipPath); + } - const count = exportZip(zipPath); + test("creates a valid zip file", async () => { + const count = await writeZip(); expect(count).toBeGreaterThan(0); expect(existsSync(zipPath)).toBe(true); }); test("zip contains expected files", async () => { + await writeZip(); const AdmZip = (await import("adm-zip")).default; - const zipPath = resolve(TEST_HOME, "test-export.zip"); const zip = new AdmZip(zipPath); const entries = zip.getEntries().map((e) => e.entryName); diff --git a/test/install-smoke.test.ts b/test/install-smoke.test.ts index 1c91952..2c0c499 100644 --- a/test/install-smoke.test.ts +++ b/test/install-smoke.test.ts @@ -144,10 +144,11 @@ describe("pal cli install (smoke)", () => { }, 90000); test("install is idempotent — second run preserves files", () => { + expect(pal("cli", "install", "--claude").status).toBe(0); const before = readdirSync(resolve(CLAUDE_DIR, "skills")).length; const result = pal("cli", "install", "--claude"); expect(result.status).toBe(0); const after = readdirSync(resolve(CLAUDE_DIR, "skills")).length; expect(after).toBe(before); - }, 90000); + }, 180000); }); diff --git a/test/signals.test.ts b/test/signals.test.ts index c19d2d4..8051dec 100644 --- a/test/signals.test.ts +++ b/test/signals.test.ts @@ -1,11 +1,11 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeEach, describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; import { resolve } from "node:path"; import { emitRating } from "../src/hooks/lib/signals"; const TEST_HOME = resolve(import.meta.dir, "../.test-home-signals"); -beforeAll(() => { +beforeEach(() => { process.env.PAL_HOME = TEST_HOME; if (existsSync(TEST_HOME)) rmSync(TEST_HOME, { recursive: true }); mkdirSync(TEST_HOME, { recursive: true }); diff --git a/test/subagent-link.test.ts b/test/subagent-link.test.ts index 2cc8779..a27b46a 100644 --- a/test/subagent-link.test.ts +++ b/test/subagent-link.test.ts @@ -48,6 +48,8 @@ copilot: You are a test helper subagent. `; +let firstLink: ReturnType; + beforeAll(() => { if (existsSync(HOME)) rmSync(HOME, { recursive: true }); // A personal subagent already authored under ~/.pal/agents/ @@ -57,6 +59,7 @@ beforeAll(() => { mkdirSync(resolve(HOME, ".claude/agents"), { recursive: true }); mkdirSync(resolve(HOME, ".config/opencode/agents"), { recursive: true }); mkdirSync(resolve(HOME, ".copilot/agents"), { recursive: true }); + firstLink = subagentLink("my-helper"); }); afterAll(() => { @@ -65,8 +68,7 @@ afterAll(() => { describe("pal cli subagent link", () => { test("installs a personal subagent into installed agents only", () => { - const res = subagentLink("my-helper"); - expect(res.status).toBe(0); + expect(firstLink.status).toBe(0); expect(existsSync(claudeFile)).toBe(true); expect(existsSync(opencodeFile)).toBe(true); diff --git a/test/token-usage.test.ts b/test/token-usage.test.ts index 15afae3..8beacb4 100644 --- a/test/token-usage.test.ts +++ b/test/token-usage.test.ts @@ -1,11 +1,11 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeEach, describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; import { resolve } from "node:path"; import { logTokenUsage } from "../src/hooks/lib/token-usage"; const TEST_HOME = resolve(import.meta.dir, "../.test-home-token-usage"); -beforeAll(() => { +beforeEach(() => { process.env.PAL_HOME = TEST_HOME; if (existsSync(TEST_HOME)) rmSync(TEST_HOME, { recursive: true }); mkdirSync(TEST_HOME, { recursive: true }); @@ -53,6 +53,8 @@ describe("logTokenUsage", () => { }); test("appends multiple entries", () => { + logTokenUsage("rating", { inputTokens: 1, outputTokens: 1 }); + const logPath = resolve(TEST_HOME, "memory", "signals", "token-usage.jsonl"); const before = readFileSync(logPath, "utf-8").trim().split("\n").length; From 2d0d29496f9503fb09e976d88a66e37526d0d1f1 Mon Sep 17 00:00:00 2001 From: Richard Kovacs Date: Tue, 18 Aug 2026 10:17:17 +0200 Subject: [PATCH 03/35] chore: add stryker mutation testing with a diff-scoped pr gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add stryker.config.mjs mutating hook libraries, agent tools, and target helpers - add .agents/scripts/mutate-diff.ts to mutate only the changed line ranges - add test:mutate and test:mutate:diff scripts - add a mutation workflow gating pull requests on ubuntu with bun 1.3.13 - register the config with biome and the script with knip - ignore the mutation sandbox in biome, typescript, and git Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .agents/scripts/mutate-diff.ts | 97 ++++++++ .github/workflows/mutation.yml | 50 +++++ .gitignore | 5 + biome.json | 4 +- bun.lock | 394 ++++++++++++++++++++------------- knip.json | 9 +- package.json | 4 + stryker.config.mjs | 96 ++++++++ tsconfig.json | 5 +- 9 files changed, 503 insertions(+), 161 deletions(-) create mode 100644 .agents/scripts/mutate-diff.ts create mode 100644 .github/workflows/mutation.yml create mode 100644 stryker.config.mjs diff --git a/.agents/scripts/mutate-diff.ts b/.agents/scripts/mutate-diff.ts new file mode 100644 index 0000000..a9b3823 --- /dev/null +++ b/.agents/scripts/mutate-diff.ts @@ -0,0 +1,97 @@ +import { spawnSync } from "node:child_process"; +import { Glob } from "bun"; +import strykerConfig from "../../stryker.config.mjs"; + +const baseRef = process.argv[2] ?? "origin/main"; + +function mutatePatterns(): string[] { + const patterns = strykerConfig.mutate ?? []; + return patterns.filter((pattern): pattern is string => typeof pattern === "string"); +} + +type MutateFilter = { includes: Glob[]; excludes: Glob[] }; + +function mutateFilter(patterns: string[]): MutateFilter { + const includes: Glob[] = []; + const excludes: Glob[] = []; + for (const pattern of patterns) { + if (pattern.startsWith("!")) { + excludes.push(new Glob(pattern.slice(1))); + } else { + includes.push(new Glob(pattern)); + } + } + return { includes, excludes }; +} + +function isMutatable(path: string, filter: MutateFilter): boolean { + if (filter.excludes.some((glob) => glob.match(path))) { + return false; + } + return filter.includes.some((glob) => glob.match(path)); +} + +function diffAgainstBase(): string { + const r = spawnSync( + "git", + ["diff", "--unified=0", "--diff-filter=ACMR", `${baseRef}...HEAD`], + { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 } + ); + if (r.status !== 0) { + process.stderr.write(r.stderr || `git diff against ${baseRef} failed\n`); + process.exit(1); + } + return r.stdout; +} + +function changedRanges(diff: string, filter: MutateFilter): string[] { + const ranges: string[] = []; + let current: string | null = null; + + for (const line of diff.split("\n")) { + const fileMatch = /^\+\+\+ b\/(.+)$/.exec(line); + if (fileMatch) { + const path = fileMatch[1]; + current = isMutatable(path, filter) ? path : null; + continue; + } + + if (!current) continue; + + const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line); + if (!hunk) continue; + + const start = Number(hunk[1]); + const count = hunk[2] === undefined ? 1 : Number(hunk[2]); + if (count === 0) continue; + + ranges.push(`${current}:${start}-${start + count - 1}`); + } + + return ranges; +} + +function runStryker(ranges: string[]): number { + const r = spawnSync("bunx", ["stryker", "run", "--mutate", ranges.join(",")], { + stdio: "inherit", + }); + return r.status ?? 1; +} + +const patterns = mutatePatterns(); +const ranges = changedRanges(diffAgainstBase(), mutateFilter(patterns)); + +process.stderr.write( + `Mutation gate covers ${patterns.length} glob(s): ${patterns.join(", ")}\n` + + `Changes outside these paths are NOT mutation-tested.\n` +); + +if (ranges.length === 0) { + process.stderr.write(`No mutatable changes against ${baseRef} — nothing to gate.\n`); + process.exit(0); +} + +process.stderr.write( + `Mutating ${ranges.length} changed range(s):\n${ranges.join("\n")}\n` +); +process.exit(runStryker(ranges)); diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml new file mode 100644 index 0000000..edb9a63 --- /dev/null +++ b/.github/workflows/mutation.yml @@ -0,0 +1,50 @@ +name: mutation + +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + mutation: + # Linux only, deliberately: the rest of CI runs a three-OS matrix, but this gate + # measures test strength, which is platform-independent. Running it three times + # would triple the slowest job in the repo for no extra signal. + runs-on: ubuntu-latest + + env: + PAL_SKIP_BROWSER_INSTALL: "1" + PAL_SKIP_DOCTOR: "1" + STRYKER_CONCURRENCY: "4" + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + # Full history so the merge-base against the PR base branch resolves. + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + # Hard pin, not a preference: bun 1.3.14 changed async-promise cleanup in a + # way that breaks the Stryker bun runner. Do not float this to latest. + bun-version: "1.3.13" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Mutation-test the changed lines + run: bun run test:mutate:diff "origin/${{ github.event.pull_request.base.ref }}" + + - name: Upload mutation report + if: always() + uses: actions/upload-artifact@v4 + with: + name: mutation-report + path: reports/mutation.html + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index fc68df8..5fa55e0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,8 @@ assets/skills/consulting-report/demo/diagrams-compressed/ # Skill tools compiled to .mjs for Node (scripts/build-skill-tools.ts). Generated at # `prepack` so the published tarball ships them; never committed. See the script header. assets/skills/*/tools/*.mjs + +# Mutation testing sandbox + reports +.stryker-tmp/ +stryker.log +reports/ diff --git a/biome.json b/biome.json index 2321a93..249f983 100644 --- a/biome.json +++ b/biome.json @@ -22,7 +22,9 @@ "!**/next-env.d.ts", "!memory", "!telos", - "klint.rules.ts" + "klint.rules.ts", + "stryker.config.mjs", + "!.stryker-tmp" ] }, "formatter": { diff --git a/bun.lock b/bun.lock index 545e47c..0785a66 100644 --- a/bun.lock +++ b/bun.lock @@ -16,11 +16,13 @@ "@biomejs/biome": "2.4.15", "@commitlint/cli": "21.0.1", "@commitlint/config-conventional": "21.0.1", + "@hughescr/stryker-bun-runner": "1.3.8", "@konvert7/klint": "0.20.0", "@opencode-ai/plugin": "latest", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.8", + "@stryker-mutator/core": "9.6.1", "@types/adm-zip": "^0.5.8", "@types/bun": "latest", "@types/node": "latest", @@ -189,17 +191,71 @@ "@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="], - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-decorators": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg=="], + + "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], + + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="], + + "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], @@ -337,6 +393,8 @@ "@huggingface/transformers": ["@huggingface/transformers@4.2.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", "sharp": "^0.34.5" } }, "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ=="], + "@hughescr/stryker-bun-runner": ["@hughescr/stryker-bun-runner@1.3.8", "", { "dependencies": { "@stryker-mutator/api": "9.6.1", "smol-toml": "1.7.0", "tinyglobby": "0.2.17", "ws": "8.21.0" }, "peerDependencies": { "@stryker-mutator/core": "^9.0.0" } }, "sha512-WxDLdHQW/ZxrvhapBjbHj1P+s/hARwiD6tlIP6w3vmDaSDHBk0jVFwChj0T5BdWlz/ZttTtcBWuHCkgRiQu5zg=="], + "@ibm-cloud/watsonx-ai": ["@ibm-cloud/watsonx-ai@1.7.13", "", { "dependencies": { "form-data": "^4.0.4", "ibm-cloud-sdk-core": "^5.4.14" } }, "sha512-xduIj2subUermAwmz3mz5LXlLfUVyN+NwzswsIkl5EmU4t4VJ/040gtdbsjmwWrwKBA6xPxTt3i1NAEaFVvahA=="], "@ibm-generative-ai/node-sdk": ["@ibm-generative-ai/node-sdk@3.2.4", "", { "dependencies": { "@ai-zen/node-fetch-event-source": "^2.1.2", "fetch-retry": "^5.0.6", "http-status-codes": "^2.3.0", "openapi-fetch": "^0.8.2", "p-queue-compat": "1.0.225", "yaml": "^2.3.3" }, "peerDependencies": { "@langchain/core": ">=0.1.0" }, "optionalPeers": ["@langchain/core"] }, "sha512-HvJSYql3lOPYZcGb23mBw0kcWLlCX+n7EDRgJQxz7gIzx9WafUuDyl1IlTCXGfxolm0EhNIub79u9v7owtks0w=="], @@ -401,18 +459,38 @@ "@inquirer/editor": ["@inquirer/editor@5.2.2", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/external-editor": "^3.0.3", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg=="], + "@inquirer/expand": ["@inquirer/expand@5.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g=="], + "@inquirer/external-editor": ["@inquirer/external-editor@3.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA=="], "@inquirer/figures": ["@inquirer/figures@2.0.7", "", {}, "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw=="], "@inquirer/input": ["@inquirer/input@5.1.2", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg=="], + "@inquirer/number": ["@inquirer/number@4.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA=="], + + "@inquirer/password": ["@inquirer/password@5.1.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg=="], + + "@inquirer/prompts": ["@inquirer/prompts@8.5.2", "", { "dependencies": { "@inquirer/checkbox": "^5.2.1", "@inquirer/confirm": "^6.1.1", "@inquirer/editor": "^5.2.2", "@inquirer/expand": "^5.1.1", "@inquirer/input": "^5.1.2", "@inquirer/number": "^4.1.1", "@inquirer/password": "^5.1.1", "@inquirer/rawlist": "^5.3.1", "@inquirer/search": "^4.2.1", "@inquirer/select": "^5.2.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@5.3.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og=="], + "@inquirer/search": ["@inquirer/search@4.2.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g=="], "@inquirer/select": ["@inquirer/select@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw=="], "@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@jscpd/badge-reporter": ["@jscpd/badge-reporter@4.2.3", "", { "dependencies": { "badgen": "^3.2.3", "colors": "^1.4.0", "fs-extra": "^11.2.0" } }, "sha512-yNvbwWl/NwogHT5XrHyqXgF9yVZeLWA2QOhGqYTopvgi7LsSbDumpOqOcJMHP9Z4RalhMfahh+dVXFSI7tMcaA=="], "@jscpd/core": ["@jscpd/core@4.2.3", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-VQ2gH+tiI51ty3PBRD4HClNNgyX/VH9cs0dcFKuywxDzLQ64jYp7vhJPcqnyiVX9tVEIAa12sucRHQP/VHwugA=="], @@ -783,6 +861,14 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@stryker-mutator/api": ["@stryker-mutator/api@9.6.1", "", { "dependencies": { "mutation-testing-metrics": "3.7.3", "mutation-testing-report-schema": "3.7.3", "tslib": "~2.8.0", "typed-inject": "~5.0.0" } }, "sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg=="], + + "@stryker-mutator/core": ["@stryker-mutator/core@9.6.1", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@stryker-mutator/api": "9.6.1", "@stryker-mutator/instrumenter": "9.6.1", "@stryker-mutator/util": "9.6.1", "ajv": "~8.18.0", "chalk": "~5.6.0", "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.6.0", "execa": "~9.6.0", "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", "minimatch": "~10.2.4", "mutation-server-protocol": "~0.4.0", "mutation-testing-elements": "3.7.3", "mutation-testing-metrics": "3.7.3", "mutation-testing-report-schema": "3.7.3", "npm-run-path": "~6.0.0", "progress": "~2.0.3", "rxjs": "~7.8.1", "semver": "^7.6.3", "source-map": "~0.7.4", "tree-kill": "~1.2.2", "tslib": "2.8.1", "typed-inject": "~5.0.0", "typed-rest-client": "~2.3.0" }, "bin": { "stryker": "bin/stryker.js" } }, "sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ=="], + + "@stryker-mutator/instrumenter": ["@stryker-mutator/instrumenter@9.6.1", "", { "dependencies": { "@babel/core": "~7.29.0", "@babel/generator": "~7.29.0", "@babel/parser": "~7.29.0", "@babel/plugin-proposal-decorators": "~7.29.0", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/preset-typescript": "~7.28.0", "@stryker-mutator/api": "9.6.1", "@stryker-mutator/util": "9.6.1", "angular-html-parser": "~10.4.0", "semver": "~7.7.0", "tslib": "2.8.1", "weapon-regex": "~1.3.2" } }, "sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA=="], + + "@stryker-mutator/util": ["@stryker-mutator/util@9.6.1", "", {}, "sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ=="], + "@swc/core": ["@swc/core@1.15.40", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.26" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.40", "@swc/core-darwin-x64": "1.15.40", "@swc/core-linux-arm-gnueabihf": "1.15.40", "@swc/core-linux-arm64-gnu": "1.15.40", "@swc/core-linux-arm64-musl": "1.15.40", "@swc/core-linux-ppc64-gnu": "1.15.40", "@swc/core-linux-s390x-gnu": "1.15.40", "@swc/core-linux-x64-gnu": "1.15.40", "@swc/core-linux-x64-musl": "1.15.40", "@swc/core-win32-arm64-msvc": "1.15.40", "@swc/core-win32-ia32-msvc": "1.15.40", "@swc/core-win32-x64-msvc": "1.15.40" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.40", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ=="], @@ -881,6 +967,8 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -925,6 +1013,8 @@ "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="], + "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], @@ -953,6 +1043,8 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], @@ -973,6 +1065,8 @@ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + "canvas": ["canvas@2.11.2", "", { "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", "nan": "^2.17.0", "simple-get": "^3.0.3" } }, "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -1023,7 +1117,7 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], @@ -1057,6 +1151,8 @@ "convert-hrtime": ["convert-hrtime@5.0.0", "", {}, "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -1113,10 +1209,14 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], "doctypes": ["doctypes@1.1.0", "", {}, "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="], @@ -1139,6 +1239,8 @@ "effect": ["effect@4.0.0-beta.65", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-QYKvQPAj3CmtsvWkHQww15wX4KG2gNsszDWEcOO5sZCMknp66u6Si/Opmt3wwWCwsyvRmDAdIg+JIz5qzbbFIw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.409", "", {}, "sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "emojilib": ["emojilib@2.4.0", "", {}, "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="], @@ -1311,6 +1413,8 @@ "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], @@ -1479,6 +1583,8 @@ "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-rouge": ["js-rouge@3.2.0", "", {}, "sha512-2dvY28iFq5NcwxPNzc2zMgLVJED843m6CnKrCy0jYnOKd+QQhdkxI1wmdQspbcOAggo3K3gUZfhTSwmM+lWoBA=="], "js-stringify": ["js-stringify@1.0.2", "", {}, "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g=="], @@ -1491,12 +1597,16 @@ "jscpd-sarif-reporter": ["jscpd-sarif-reporter@4.2.3", "", { "dependencies": { "colors": "^1.4.0", "fs-extra": "^11.2.0", "node-sarif-builder": "^3.4.0" } }, "sha512-rM0LM5S0kdASLCtDsr1s51rJOPf8nubaxaWQUTWVVPda1UMPymXbELG+A3Rgpoa4D4QFUFfXqz60Jn/W+vlFtA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-rpc-2.0": ["json-rpc-2.0@1.7.1", "", {}, "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], @@ -1559,6 +1669,8 @@ "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], + "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], @@ -1631,6 +1743,8 @@ "mimic-response": ["mimic-response@2.1.0", "", {}, "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1661,6 +1775,14 @@ "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + "mutation-server-protocol": ["mutation-server-protocol@0.4.1", "", { "dependencies": { "zod": "^4.1.12" } }, "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g=="], + + "mutation-testing-elements": ["mutation-testing-elements@3.7.3", "", {}, "sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ=="], + + "mutation-testing-metrics": ["mutation-testing-metrics@3.7.3", "", { "dependencies": { "mutation-testing-report-schema": "3.7.3" } }, "sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ=="], + + "mutation-testing-report-schema": ["mutation-testing-report-schema@3.7.3", "", {}, "sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA=="], + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -1689,6 +1811,8 @@ "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + "node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="], "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], @@ -1703,7 +1827,7 @@ "npm": ["npm@11.12.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/arborist": "^9.4.2", "@npmcli/config": "^10.8.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", "@npmcli/package-json": "^7.0.5", "@npmcli/promise-spawn": "^9.0.1", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.4", "@sigstore/tuf": "^4.0.2", "abbrev": "^4.0.0", "archy": "~1.0.0", "cacache": "^20.0.4", "chalk": "^5.6.2", "ci-info": "^4.4.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", "glob": "^13.0.6", "graceful-fs": "^4.2.11", "hosted-git-info": "^9.0.2", "ini": "^6.0.0", "init-package-json": "^8.2.5", "is-cidr": "^6.0.3", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", "libnpmdiff": "^8.1.5", "libnpmexec": "^10.2.5", "libnpmfund": "^7.0.19", "libnpmorg": "^8.0.1", "libnpmpack": "^9.1.5", "libnpmpublish": "^11.1.3", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", "libnpmversion": "^8.0.3", "make-fetch-happen": "^15.0.5", "minimatch": "^10.2.4", "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", "node-gyp": "^12.2.0", "nopt": "^9.0.0", "npm-audit-report": "^7.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.2", "npm-pick-manifest": "^11.0.3", "npm-profile": "^12.0.1", "npm-registry-fetch": "^19.1.1", "npm-user-validate": "^4.0.0", "p-map": "^7.0.4", "pacote": "^21.5.0", "parse-conflict-json": "^5.0.1", "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", "read": "^5.0.1", "semver": "^7.7.4", "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", "tar": "^7.5.11", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", "validate-npm-package-name": "^7.0.2", "which": "^6.0.1" }, "bin": { "npm": "bin/npm-cli.js", "npx": "bin/npx-cli.js" } }, "sha512-xPhOap4ZbJWyd7DAOukP564WFwNSGu/2FeTRFHhiiKthcauxhH/NpkJAQm24xD+cAn8av5tQ00phi98DqtfLsg=="], - "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], "npmlog": ["npmlog@5.0.1", "", { "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", "gauge": "^3.0.0", "set-blocking": "^2.0.0" } }, "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw=="], @@ -1803,7 +1927,7 @@ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -1869,6 +1993,8 @@ "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], @@ -1919,7 +2045,7 @@ "python-shell": ["python-shell@5.0.0", "", {}, "sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w=="], - "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], @@ -2045,7 +2171,7 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + "smol-toml": ["smol-toml@1.7.0", "", {}, "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ=="], "socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="], @@ -2059,7 +2185,7 @@ "socks-proxy-agent": ["socks-proxy-agent@10.0.0", "", { "dependencies": { "agent-base": "9.0.0", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-pyp2YR3mNxAMu0mGLtzs4g7O3uT4/9sQOLAKcViAkaS9fJWkud7nmaf6ZREFqQEi24IPkBcjfHjXhPTUWjo3uA=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "spark-md5": ["spark-md5@3.0.2", "", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="], @@ -2145,7 +2271,7 @@ "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -2163,11 +2289,13 @@ "traverse": ["traverse@0.6.8", "", {}, "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], - "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], @@ -2179,6 +2307,10 @@ "typed-function": ["typed-function@4.2.2", "", {}, "sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A=="], + "typed-inject": ["typed-inject@5.0.0", "", {}, "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA=="], + + "typed-rest-client": ["typed-rest-client@2.3.1", "", { "dependencies": { "des.js": "^1.1.0", "js-md4": "^0.3.2", "qs": "6.15.1", "tunnel": "0.0.6", "underscore": "^1.13.8" } }, "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], @@ -2209,6 +2341,8 @@ "unzipper": ["unzipper@0.12.3", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "^11.2.0", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA=="], + "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], + "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], @@ -2229,6 +2363,8 @@ "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "web-worker": ["web-worker@1.5.0", "", {}, "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw=="], @@ -2285,125 +2421,31 @@ "@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], - "@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-crypto/crc32c/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-crypto/sha1-browser/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-crypto/sha256-browser/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-crypto/sha256-js/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-crypto/supports-web-crypto/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-crypto/util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/checksums/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/client-bedrock-agent-runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/client-bedrock-runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/client-s3/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/client-sagemaker-runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/credential-provider-env/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/credential-provider-http/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/credential-provider-ini/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/credential-provider-login/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/credential-provider-node/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/credential-provider-process/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1060.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.17", "@aws-sdk/nested-clients": "^3.997.15", "@aws-sdk/types": "^3.973.10", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-6NZaMKkFhpaNiwLpHi1sZaYjidL/lCJE6ME6NxwA8gv9vQna+Kr0j4OFwVoz6tANRWM3WbGz6jiPsGX/Vkjwow=="], - "@aws-sdk/credential-provider-sso/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/credential-provider-web-identity/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/eventstream-handler-node/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/middleware-bucket-endpoint/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/middleware-eventstream/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/middleware-expect-continue/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/middleware-flexible-checksums/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/middleware-location-constraint/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/middleware-sdk-s3/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/middleware-ssec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/middleware-websocket/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/nested-clients/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/signature-v4-multi-region/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/token-providers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@aws-sdk/util-locate-window/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "@aws-sdk/xml-builder/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure-rest/core-client/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/abort-controller/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/ai-projects/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-auth/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-client/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-lro/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-paging/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-rest-pipeline/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-sse/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-tracing/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@azure/core-xml/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@azure/openai-assistants/@azure-rest/core-client": ["@azure-rest/core-client@1.4.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.5.0", "@azure/core-tracing": "^1.0.1", "@azure/core-util": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-ozTDPBVUDR5eOnMIwhggbnVmOrka4fXCs8n8mvUo4WLLc38kki6bAOByDoVZZPz/pZy2jMt2kwfpvy/UjALj6w=="], - "@azure/identity/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@azure/storage-blob/@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], - "@azure/logger/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@babel/core/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], - "@azure/openai-assistants/@azure-rest/core-client": ["@azure-rest/core-client@1.4.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.5.0", "@azure/core-tracing": "^1.0.1", "@azure/core-util": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-ozTDPBVUDR5eOnMIwhggbnVmOrka4fXCs8n8mvUo4WLLc38kki6bAOByDoVZZPz/pZy2jMt2kwfpvy/UjALj6w=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@azure/openai-assistants/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@babel/generator/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], - "@azure/storage-blob/@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@azure/storage-blob/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@azure/storage-common/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@babel/template/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], - "@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@babel/traverse/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -2421,6 +2463,8 @@ "@semantic-release/github/aggregate-error": ["aggregate-error@5.0.0", "", { "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" } }, "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw=="], + "@semantic-release/github/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "@semantic-release/npm/@semantic-release/error": ["@semantic-release/error@4.0.0", "", {}, "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ=="], "@semantic-release/npm/aggregate-error": ["aggregate-error@5.0.0", "", { "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" } }, "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw=="], @@ -2431,25 +2475,7 @@ "@semantic-release/release-notes-generator/read-package-up": ["read-package-up@11.0.0", "", { "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", "type-fest": "^4.6.0" } }, "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ=="], - "@smithy/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@smithy/credential-provider-imds/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@smithy/fetch-http-handler/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@smithy/is-array-buffer/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@smithy/node-http-handler/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@smithy/signature-v4/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@smithy/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@smithy/util-buffer-from/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@smithy/util-utf8/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@stryker-mutator/core/execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], "@types/adm-zip/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], @@ -2457,18 +2483,18 @@ "@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "@typespec/ts-http-runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "babel-walk/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "blamer/execa": ["execa@4.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA=="], + "body-parser/qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "cli-highlight/parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], @@ -2493,10 +2519,14 @@ "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "constantinople/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "cosmiconfig-typescript-loader/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], "duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -2509,10 +2539,16 @@ "env-ci/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "eventsource/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + "execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "express/qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + "from2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -2525,6 +2561,10 @@ "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@8.0.0", "", {}, "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ=="], + "googleapis-common/qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + + "handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "hosted-git-info/lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], "ibm-cloud-sdk-core/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], @@ -2537,8 +2577,16 @@ "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "jscpd/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + "jstransformer/is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], + "keyv-file/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "knip/smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + + "knip/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "lint-staged/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], @@ -2843,6 +2891,8 @@ "npm/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "nunjucks/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], "onnxruntime-web/protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], @@ -2853,13 +2903,15 @@ "p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], "path-scurry/lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], - "pgpass/split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "promptfoo/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "pgpass/split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], "promptfoo/execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], @@ -2881,8 +2933,6 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "rxjs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "semantic-release/@semantic-release/error": ["@semantic-release/error@4.0.0", "", {}, "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ=="], "semantic-release/@semantic-release/github": ["@semantic-release/github@12.0.6", "", { "dependencies": { "@octokit/core": "^7.0.0", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-retry": "^8.0.0", "@octokit/plugin-throttling": "^11.0.0", "@semantic-release/error": "^4.0.0", "aggregate-error": "^5.0.0", "debug": "^4.3.4", "dir-glob": "^3.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "issue-parser": "^7.0.0", "lodash-es": "^4.17.21", "mime": "^4.0.0", "p-filter": "^4.0.0", "tinyglobby": "^0.2.14", "undici": "^7.0.0", "url-join": "^5.0.0" }, "peerDependencies": { "semantic-release": ">=24.1.0" } }, "sha512-aYYFkwHW3c6YtHwQF0t0+lAjlU+87NFOZuH2CvWFD0Ylivc7MwhZMiHOJ0FMpIgPpCVib/VUAcOwvrW0KnxQtA=="], @@ -2935,8 +2985,16 @@ "winston/@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + "with/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "wrap-ansi/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@mapbox/node-pre-gyp/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "@semantic-release/github/aggregate-error/clean-stack": ["clean-stack@5.3.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg=="], @@ -2953,8 +3011,6 @@ "@semantic-release/npm/execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - "@semantic-release/npm/execa/npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - "@semantic-release/npm/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "@semantic-release/npm/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], @@ -2963,6 +3019,16 @@ "@semantic-release/release-notes-generator/read-package-up/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "@stryker-mutator/core/execa/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "@stryker-mutator/core/execa/human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "@stryker-mutator/core/execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "@stryker-mutator/core/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "@stryker-mutator/core/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "@types/adm-zip/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@typespec/ts-http-runtime/http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -2973,10 +3039,16 @@ "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "babel-walk/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "babel-walk/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "blamer/execa/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], "blamer/execa/human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="], + "blamer/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], @@ -2997,6 +3069,10 @@ "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "constantinople/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "constantinople/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "duplexer2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "engine.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -3015,6 +3091,8 @@ "env-ci/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + "execa/npm-run-path/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "from2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -3039,18 +3117,20 @@ "npm/minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "promptfoo/execa/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], "promptfoo/execa/human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], "promptfoo/execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - "promptfoo/execa/npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - "promptfoo/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "promptfoo/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "read-pkg/parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "read-pkg/parse-json/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -3071,8 +3151,6 @@ "semantic-release/execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - "semantic-release/execa/npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - "semantic-release/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "semantic-release/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], @@ -3099,7 +3177,9 @@ "wide-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@semantic-release/npm/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "with/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "with/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], @@ -3107,6 +3187,8 @@ "@semantic-release/release-notes-generator/read-package-up/read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], + "blamer/execa/npm-run-path/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "cli-highlight/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -3121,15 +3203,13 @@ "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "env-ci/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "env-ci/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], "npm/minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "npm/minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "promptfoo/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "read-pkg/parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], @@ -3139,14 +3219,14 @@ "semantic-release/@semantic-release/github/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "semantic-release/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "signale/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], + "@semantic-release/release-notes-generator/read-package-up/read-pkg/parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "cli-highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -3159,6 +3239,8 @@ "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "@semantic-release/release-notes-generator/read-package-up/read-pkg/parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], diff --git a/knip.json b/knip.json index 32d8455..63a3209 100644 --- a/knip.json +++ b/knip.json @@ -8,6 +8,7 @@ "src/targets/**/install.ts", "src/targets/**/uninstall.ts", ".agents/hooks/*.ts", + ".agents/scripts/*.ts", "assets/skills/*/tools/*.ts", ".opencode/plugins/*.ts" ], @@ -15,13 +16,17 @@ "src/**/*.ts" ], "includeEntryExports": true, - "tags": ["-lintignore"], + "tags": [ + "-lintignore" + ], "ignore": [ ".opencode/plugins/lint.ts", "src/targets/opencode/plugin.ts" ], "ignoreDependencies": [ "@semantic-release/github", - "promptfoo" + "promptfoo", + "@stryker-mutator/bun-runner", + "@stryker-mutator/api" ] } diff --git a/package.json b/package.json index 9e9dbc2..d18e8c9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "scripts": { "type-check": "tsc --noEmit", "test": "bun test --randomize", + "test:mutate": "stryker run", + "test:mutate:diff": "bun .agents/scripts/mutate-diff.ts", "format": "biome format", "format-write": "biome format --write", "lint": "biome lint", @@ -66,11 +68,13 @@ "@biomejs/biome": "2.4.15", "@commitlint/cli": "21.0.1", "@commitlint/config-conventional": "21.0.1", + "@hughescr/stryker-bun-runner": "1.3.8", "@konvert7/klint": "0.20.0", "@opencode-ai/plugin": "latest", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.8", + "@stryker-mutator/core": "9.6.1", "@types/adm-zip": "^0.5.8", "@types/bun": "latest", "@types/node": "latest", diff --git a/stryker.config.mjs b/stryker.config.mjs new file mode 100644 index 0000000..d20c5a6 --- /dev/null +++ b/stryker.config.mjs @@ -0,0 +1,96 @@ +import { readdirSync } from "node:fs"; + +// Stryker switches a mutant on through an in-process global, which a child process +// never sees. These suites drive the code under test with Bun.spawn, so their mutants +// score as uncovered no matter how well they assert — leaving them out keeps the ring +// honest instead of padding it with phantom survivors. +const SUBPROCESS_SUITES = new Set([ + "test/cli.test.ts", + "test/export.test.ts", + "test/flagship-author.test.ts", + "test/import-merge.test.ts", + "test/install-smoke.test.ts", + "test/package-publish.test.ts", + "test/project-cli.test.ts", + "test/rtk-wrap.test.ts", + "test/security-tool-names.test.ts", + "test/skill-doctor.test.ts", + "test/skill-link.test.ts", + "test/spawn-guard.test.ts", + "test/subagent-link.test.ts", + "test/update-command.test.ts", +]); + +function isInProcessSuite(file) { + return file.endsWith(".test.ts") && !SUBPROCESS_SUITES.has(file); +} + +// The bun runner passes testFiles to `bun test` verbatim and never expands globs, so a +// pattern here silently matches nothing and the run dies on an inspector timeout. +const testFiles = readdirSync("test", { recursive: true }) + .map(String) + .map((file) => `test/${file}`) + .filter(isInProcessSuite) + .sort(); + +/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ +export default { + plugins: ["@hughescr/stryker-bun-runner"], + testRunner: "bun", + coverageAnalysis: "perTest", + // The ring is the portable core the suite exercises in-process: hook libraries, agent + // tools, and the shared target helpers. Everything left out is either an entrypoint the + // suite only reaches by spawning it (src/cli, src/hooks/*.ts) or an OS/network boundary + // that is stubbed away before a mutant could ever be observed. + mutate: [ + "src/hooks/lib/**/*.ts", + "src/tools/**/*.ts", + "src/targets/lib.ts", + "!src/hooks/lib/inference.ts", + "!src/hooks/lib/notify.ts", + "!src/hooks/lib/stdin.ts", + "!src/hooks/lib/which.ts", + ], + concurrency: Number(process.env.STRYKER_CONCURRENCY ?? 4), + bun: { + testFiles, + // No --isolate: bun re-runs the preload and rebuilds the module graph for every file, + // and this runner's dry-run preload eager-imports every mutated module each time. + timeout: 120000, + // Loading the whole suite pushes the runner's inspector handshake past its 5s default. + inspectorTimeout: 60000, + }, + reporters: ["clear-text", "progress", "html"], + // Measured 2026-08-18 over the whole ring: 10823 mutants, 37.43% total / 70.90% of + // covered, 0 errors. `break` stays null until the CLI-entrypoint tools under + // src/tools/ have in-process tests — a threshold guessed before that just gets + // disabled the first time it fires. The PR gate reports; it does not yet block. + thresholds: { high: 80, low: 60, break: null }, + // Stryker copies the project into a sandbox with fs.copyFile, which throws ENOTSUP on a + // symlink. Every entry below is either a symlink farm (agent config dirs, the installed + // test homes, the vendored skill node_modules) or bulk the suite never reads. + ignorePatterns: [ + // Kept, minus its one symlink: algorithm-review.test.ts asserts on the real presence + // of .agents/skills/algorithm-update/SKILL.md to detect a maintainer checkout. + ".agents/skills/klint-rules", + ".claude", + ".codex", + ".cursor", + ".github", + ".husky", + ".opencode", + ".test-home*", + ".test-install-home", + ".test-tmp", + "backups", + "docs", + "eval", + "**/node_modules", + "pal-export-*.zip", + ], + tempDirName: ".stryker-tmp", + // Always clean: a crashed run otherwise leaves sandbox copies of biome.json behind, + // which biome then rejects as nested root configurations. + cleanTempDir: "always", + htmlReporter: { fileName: "reports/mutation.html" }, +}; diff --git a/tsconfig.json b/tsconfig.json index 9f84d62..a050a7b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,7 @@ "node_modules", "assets", "memory", - "telos" + "telos", + ".stryker-tmp" ] -} \ No newline at end of file +} From 60646b1841d0e2d6c45f35afd014675cf6ecc73c Mon Sep 17 00:00:00 2001 From: Richard Kovacs Date: Tue, 18 Aug 2026 10:33:49 +0200 Subject: [PATCH 04/35] test(targets): cover the settings, cursor-hook, and vscode-path helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assert the settings merge/unmerge round-trip returns user settings intact - cover hook path canonicalization, permission dedup, and per-key user precedence - cover the cursor hook merge/unmerge pair including exact-match uninstall - cover vscodeSettingsFile across macos, linux, windows, and unsupported platforms Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/cursor-hooks-merge.test.ts | 130 ++++++++++++++++ test/merge-settings.test.ts | 246 +++++++++++++++++++++++++++++- test/vscode-settings-path.test.ts | 80 ++++++++++ 3 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 test/cursor-hooks-merge.test.ts create mode 100644 test/vscode-settings-path.test.ts diff --git a/test/cursor-hooks-merge.test.ts b/test/cursor-hooks-merge.test.ts new file mode 100644 index 0000000..45e236c --- /dev/null +++ b/test/cursor-hooks-merge.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, test } from "bun:test"; +import { mergeCursorHooks, unmergeCursorHooks } from "../src/targets/lib"; + +const TEMPLATE = { + version: 1, + hooks: { + beforeShellExecution: [ + { type: "command", command: "bun run /pkg/src/hooks/SecurityValidator.ts" }, + ], + stop: [ + { + type: "command", + command: "bun run /pkg/src/hooks/StopOrchestrator.ts", + timeout: 30, + }, + ], + }, +}; + +describe("mergeCursorHooks", () => { + test("installs every template event into empty hooks", () => { + const merged = mergeCursorHooks({}, TEMPLATE); + + expect(merged.version).toBe(1); + expect(Object.keys(merged.hooks ?? {})).toEqual(["beforeShellExecution", "stop"]); + expect(merged.hooks?.stop?.[0]?.timeout).toBe(30); + }); + + test("keeps the existing version rather than resetting it", () => { + expect(mergeCursorHooks({ version: 2 }, TEMPLATE).version).toBe(2); + }); + + test("defaults the version to 1 when absent", () => { + expect(mergeCursorHooks({}, {}).version).toBe(1); + }); + + test("replaces a PAL hook installed from a different package path", () => { + const existing = { + hooks: { + stop: [ + { type: "command", command: "bun run /old/path/src/hooks/StopOrchestrator.ts" }, + ], + }, + }; + + const commands = (mergeCursorHooks(existing, TEMPLATE).hooks?.stop ?? []).map( + (e) => e.command + ); + + expect(commands).toEqual(["bun run /pkg/src/hooks/StopOrchestrator.ts"]); + }); + + test("preserves a user hook on an event PAL also uses", () => { + const existing = { + hooks: { stop: [{ type: "command", command: "echo mine" }] }, + }; + + const commands = (mergeCursorHooks(existing, TEMPLATE).hooks?.stop ?? []).map( + (e) => e.command + ); + + expect(commands).toEqual(["echo mine", "bun run /pkg/src/hooks/StopOrchestrator.ts"]); + }); + + test("leaves hooks alone when the template has none", () => { + const existing = { + hooks: { stop: [{ type: "command", command: "echo mine" }] }, + }; + + expect(mergeCursorHooks(existing, {}).hooks).toEqual(existing.hooks); + }); +}); + +describe("unmergeCursorHooks", () => { + test("removes every hook the template installed", () => { + const merged = mergeCursorHooks({}, TEMPLATE); + + expect(unmergeCursorHooks(merged, TEMPLATE).hooks).toBeUndefined(); + }); + + test("round-trips a user hook back to its original shape", () => { + const original = { + version: 1, + hooks: { stop: [{ type: "command", command: "echo mine" }] }, + }; + + const merged = mergeCursorHooks(structuredClone(original), TEMPLATE); + + expect(unmergeCursorHooks(merged, TEMPLATE)).toEqual(original); + }); + + test("keeps an unrelated event intact", () => { + const existing = { + hooks: { + afterFileEdit: [{ type: "command", command: "echo mine" }], + stop: [ + { + type: "command", + command: "bun run /pkg/src/hooks/StopOrchestrator.ts", + timeout: 30, + }, + ], + }, + }; + + expect(unmergeCursorHooks(existing, TEMPLATE).hooks).toEqual({ + afterFileEdit: [{ type: "command", command: "echo mine" }], + }); + }); + + test("matches on the exact command, so an old-path PAL hook survives uninstall", () => { + const existing = { + hooks: { + stop: [ + { type: "command", command: "bun run /old/path/src/hooks/StopOrchestrator.ts" }, + ], + }, + }; + + expect(unmergeCursorHooks(existing, TEMPLATE).hooks?.stop).toHaveLength(1); + }); + + test("leaves hooks alone when the template has none", () => { + const existing = { + hooks: { stop: [{ type: "command", command: "echo mine" }] }, + }; + + expect(unmergeCursorHooks(existing, {}).hooks).toEqual(existing.hooks); + }); +}); diff --git a/test/merge-settings.test.ts b/test/merge-settings.test.ts index 8720a2c..33e1f37 100644 --- a/test/merge-settings.test.ts +++ b/test/merge-settings.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { mergeSettings } from "../src/targets/lib"; +import { mergeSettings, unmergeSettings } from "../src/targets/lib"; describe("mergeSettings — deprecated permission cleanup", () => { test("strips ineffective Grep()/Glob() rules left by older templates", () => { @@ -38,3 +38,247 @@ describe("mergeSettings — deprecated permission cleanup", () => { expect(allow).toContain("Read(//*)"); }); }); + +const PAL_TEMPLATE = { + hooks: { + SessionStart: [ + { + hooks: [{ type: "command", command: "bun run /pkg/src/hooks/LoadContext.ts" }], + }, + ], + Stop: [ + { + matcher: "*", + hooks: [ + { type: "command", command: "bun run /pkg/src/hooks/StopOrchestrator.ts" }, + ], + }, + ], + }, + permissions: { allow: ["Read(//*)", "Bash(bun run test *)"] }, + skillOverrides: { telos: { enabled: true } }, + attribution: { commit: "Co-authored by Jarvis", pr: "Co-authored by [Jarvis]" }, + showClearContextOnPlanAccept: true, + respectGitignore: false, + spinnerTipsEnabled: false, + spinnerTipsOverride: { tips: ["pal tip one", "pal tip two"] }, +}; + +describe("mergeSettings", () => { + test("adds every template section to empty settings", () => { + const merged = mergeSettings({}, PAL_TEMPLATE); + + expect(Object.keys(merged.hooks ?? {})).toEqual(["SessionStart", "Stop"]); + expect(merged.permissions?.allow).toEqual(["Read(//*)", "Bash(bun run test *)"]); + expect(merged.skillOverrides).toEqual({ telos: { enabled: true } }); + expect(merged.attribution).toEqual(PAL_TEMPLATE.attribution); + expect(merged.showClearContextOnPlanAccept).toBe(true); + expect(merged.respectGitignore).toBe(false); + expect(merged.spinnerTipsEnabled).toBe(false); + expect(merged.spinnerTipsOverride).toEqual({ tips: ["pal tip one", "pal tip two"] }); + }); + + test("replaces a PAL hook installed from a different package path", () => { + const existing = { + hooks: { + SessionStart: [ + { + hooks: [ + { type: "command", command: "bun run /old/path/src/hooks/LoadContext.ts" }, + ], + }, + ], + }, + }; + + const merged = mergeSettings(existing, PAL_TEMPLATE); + const commands = (merged.hooks?.SessionStart ?? []).map((e) => e.hooks?.[0]?.command); + + expect(commands).toEqual(["bun run /pkg/src/hooks/LoadContext.ts"]); + }); + + test("strips a leading env assignment when comparing hook commands", () => { + const existing = { + hooks: { + SessionStart: [ + { + hooks: [ + { + type: "command", + command: "PAL_DEBUG=1 bun run /old/src/hooks/LoadContext.ts", + }, + ], + }, + ], + }, + }; + + const merged = mergeSettings(existing, PAL_TEMPLATE); + + expect(merged.hooks?.SessionStart).toHaveLength(1); + }); + + test("preserves a user hook on an event PAL also uses", () => { + const existing = { + hooks: { + Stop: [{ hooks: [{ type: "command", command: "echo mine" }] }], + }, + }; + + const merged = mergeSettings(existing, PAL_TEMPLATE); + const commands = (merged.hooks?.Stop ?? []).map((e) => e.hooks?.[0]?.command); + + expect(commands).toContain("echo mine"); + expect(commands).toContain("bun run /pkg/src/hooks/StopOrchestrator.ts"); + }); + + test("keeps a user permission and adds template permissions once", () => { + const existing = { + permissions: { allow: ["WebFetch", "Read(//*)"] }, + }; + + const merged = mergeSettings(existing, PAL_TEMPLATE); + + expect(merged.permissions?.allow).toEqual([ + "WebFetch", + "Read(//*)", + "Bash(bun run test *)", + ]); + }); + + test("keeps the user value for every key the template also sets", () => { + const existing = { + skillOverrides: { telos: { enabled: false } }, + attribution: { commit: "mine", pr: "mine" }, + showClearContextOnPlanAccept: false, + respectGitignore: true, + spinnerTipsEnabled: true, + }; + + const merged = mergeSettings(existing, PAL_TEMPLATE); + + expect(merged.skillOverrides).toEqual({ telos: { enabled: false } }); + expect(merged.attribution).toEqual({ commit: "mine", pr: "mine" }); + expect(merged.showClearContextOnPlanAccept).toBe(false); + expect(merged.respectGitignore).toBe(true); + expect(merged.spinnerTipsEnabled).toBe(true); + }); + + test("appends template tips after user tips without duplicating", () => { + const existing = { + spinnerTipsOverride: { tips: ["mine", "pal tip one"] }, + }; + + const merged = mergeSettings(existing, PAL_TEMPLATE); + + expect((merged.spinnerTipsOverride as { tips: string[] }).tips).toEqual([ + "mine", + "pal tip one", + "pal tip two", + ]); + }); + + test("leaves settings untouched when the template is empty", () => { + const existing = { + permissions: { allow: ["WebFetch"] }, + editorMode: "vim", + }; + + expect(mergeSettings(existing, {})).toEqual(existing); + }); +}); + +describe("unmergeSettings", () => { + test("removes every template section it installed", () => { + const merged = mergeSettings({}, PAL_TEMPLATE); + const cleaned = unmergeSettings(merged, PAL_TEMPLATE); + + expect(cleaned.hooks).toBeUndefined(); + expect(cleaned.permissions).toBeUndefined(); + expect(cleaned.skillOverrides).toBeUndefined(); + expect(cleaned.attribution).toBeUndefined(); + expect(cleaned.showClearContextOnPlanAccept).toBeUndefined(); + expect(cleaned.respectGitignore).toBeUndefined(); + expect(cleaned.spinnerTipsEnabled).toBeUndefined(); + expect(cleaned.spinnerTipsOverride).toBeUndefined(); + }); + + test("round-trips user settings back to their original shape", () => { + const original = { + editorMode: "vim", + hooks: { + Stop: [{ hooks: [{ type: "command", command: "echo mine" }] }], + }, + permissions: { allow: ["WebFetch"] }, + skillOverrides: { mine: { enabled: true } }, + spinnerTipsOverride: { tips: ["mine"] }, + }; + + const merged = mergeSettings(structuredClone(original), PAL_TEMPLATE); + const cleaned = unmergeSettings(merged, PAL_TEMPLATE); + + expect(cleaned).toEqual(original); + }); + + test("drops an event whose only entry was a PAL hook", () => { + const merged = mergeSettings({}, PAL_TEMPLATE); + const cleaned = unmergeSettings(merged, PAL_TEMPLATE); + + expect(cleaned.hooks).toBeUndefined(); + }); + + test("removes a PAL hook installed from a different package path", () => { + const existing = { + hooks: { + SessionStart: [ + { + hooks: [ + { type: "command", command: "bun run /old/path/src/hooks/LoadContext.ts" }, + ], + }, + ], + }, + }; + + expect(unmergeSettings(existing, PAL_TEMPLATE).hooks).toBeUndefined(); + }); + + test("keeps a user permission while removing the template ones", () => { + const existing = { + permissions: { allow: ["WebFetch", "Read(//*)", "Bash(bun run test *)"] }, + }; + + expect(unmergeSettings(existing, PAL_TEMPLATE).permissions?.allow).toEqual([ + "WebFetch", + ]); + }); + + test("keeps a user skill override while removing the template ones", () => { + const existing = { + skillOverrides: { telos: { enabled: true }, mine: { enabled: true } }, + }; + + expect(unmergeSettings(existing, PAL_TEMPLATE).skillOverrides).toEqual({ + mine: { enabled: true }, + }); + }); + + test("keeps user tips while removing the template tips", () => { + const existing = { + spinnerTipsOverride: { tips: ["mine", "pal tip one", "pal tip two"] }, + }; + + expect(unmergeSettings(existing, PAL_TEMPLATE).spinnerTipsOverride).toEqual({ + tips: ["mine"], + }); + }); + + test("leaves settings untouched when the template is empty", () => { + const existing = { + permissions: { allow: ["WebFetch"] }, + editorMode: "vim", + }; + + expect(unmergeSettings(existing, {})).toEqual(existing); + }); +}); diff --git a/test/vscode-settings-path.test.ts b/test/vscode-settings-path.test.ts new file mode 100644 index 0000000..507aa9c --- /dev/null +++ b/test/vscode-settings-path.test.ts @@ -0,0 +1,80 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { homedir } from "node:os"; +import { resolve } from "node:path"; +import { vscodeSettingsFile } from "../src/targets/lib"; + +const realPlatform = process.platform; +const savedXdg = process.env.XDG_CONFIG_HOME; +const savedAppData = process.env.APPDATA; + +function asPlatform(value: string) { + Object.defineProperty(process, "platform", { value, configurable: true }); +} + +afterEach(() => { + Object.defineProperty(process, "platform", { + value: realPlatform, + configurable: true, + }); + if (savedXdg === undefined) delete process.env.XDG_CONFIG_HOME; + else process.env.XDG_CONFIG_HOME = savedXdg; + if (savedAppData === undefined) delete process.env.APPDATA; + else process.env.APPDATA = savedAppData; +}); + +describe("vscodeSettingsFile", () => { + test("resolves under Application Support on macOS", () => { + asPlatform("darwin"); + + expect(vscodeSettingsFile()).toBe( + resolve( + homedir(), + "Library", + "Application Support", + "Code", + "User", + "settings.json" + ) + ); + }); + + test("honours XDG_CONFIG_HOME on Linux", () => { + asPlatform("linux"); + process.env.XDG_CONFIG_HOME = "/xdg"; + + expect(vscodeSettingsFile()).toBe(resolve("/xdg", "Code", "User", "settings.json")); + }); + + test("falls back to ~/.config on Linux", () => { + asPlatform("linux"); + delete process.env.XDG_CONFIG_HOME; + + expect(vscodeSettingsFile()).toBe( + resolve(homedir(), ".config", "Code", "User", "settings.json") + ); + }); + + test("honours APPDATA on Windows", () => { + asPlatform("win32"); + process.env.APPDATA = "/appdata"; + + expect(vscodeSettingsFile()).toBe( + resolve("/appdata", "Code", "User", "settings.json") + ); + }); + + test("falls back to AppData/Roaming on Windows", () => { + asPlatform("win32"); + delete process.env.APPDATA; + + expect(vscodeSettingsFile()).toBe( + resolve(homedir(), "AppData", "Roaming", "Code", "User", "settings.json") + ); + }); + + test("returns null on an unsupported platform", () => { + asPlatform("freebsd"); + + expect(vscodeSettingsFile()).toBeNull(); + }); +}); From f32b1bfe1998fd6d2c83c3c7de4208df1d0c50a8 Mon Sep 17 00:00:00 2001 From: Richard Kovacs Date: Tue, 18 Aug 2026 10:48:02 +0200 Subject: [PATCH 05/35] feat(project): add edit-isc to rewrite an ISC in place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rewrite ISC-N's text while keeping its id and open or closed state - resolve the ISC from Criteria first, then the Changelog - return the previous text alongside the new one - require replacement text Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/tools/agent/project.ts | 43 +++++++++++++++++++++ test/project-cli.test.ts | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/src/tools/agent/project.ts b/src/tools/agent/project.ts index 9ab72cb..1fee133 100644 --- a/src/tools/agent/project.ts +++ b/src/tools/agent/project.ts @@ -529,6 +529,45 @@ function cmdShowIsc(args: string[]): void { ok({ name, id: isc.id, status: isc.checked ? "closed" : "open", text: isc.text }); } +// edit-isc rewrites one ISC's text in place, keeping its id and open/done state. +// The id never leaves the record, so nextIscId still reserves it. Returns the +// previous text because the ISA files carry no version history of their own. +function cmdEditIsc(args: string[]): void { + const name = args[0]; + const id = Number(args[1]); + const text = args.slice(2).join(" ").trim(); + if (!name || !Number.isInteger(id) || id < 1 || !text) { + fail('Usage: edit-isc "new text"'); + } + const p = requireProject(name); + const inCriteria = parseIscs(p.criteria ?? "").find((i) => i.id === id); + const isc = inCriteria ?? parseIscs(p.changelog ?? "").find((i) => i.id === id); + if (!isc) fail(`ISC-${id} not found in project "${name}".`); + + const box = isc.checked ? "[x]" : "[ ]"; + const rewrite = (section: string) => + section + .split("\n") + .map((l) => + new RegExp(String.raw`^-\s+\[[ x]\]\s+ISC-${id}:`, "i").test(l) + ? `- ${box} ISC-${id}: ${text}` + : l + ) + .join("\n"); + + if (inCriteria) p.criteria = rewrite(p.criteria ?? ""); + else p.changelog = rewrite(p.changelog ?? ""); + p.updated = now(); + writeProject(p); + ok({ + edited: true, + id, + status: isc.checked ? "closed" : "open", + previous: isc.text, + text, + }); +} + // Backfill: sweep any done ISCs still sitting in Criteria (legacy projects, or // completions from before archive-on-complete) into the Changelog in one pass. function cmdPruneIsc(args: string[]): void { @@ -630,6 +669,7 @@ Commands: reopen-isc reopen ISC-N (mark not done) list-isc [--all | --closed] list open ISCs (default); --all or --closed for done show-isc print one ISC's full text + edit-isc "new text" rewrite ISC-N's text, keeping its id and state prune-isc archive done ISCs from Criteria into the Changelog isa-init mark project as ISA-initialized scaffold-task-isa create a one-shot task ISA in memory/work/ @@ -725,6 +765,9 @@ function run(): void { case "show-isc": cmdShowIsc(rest); return; + case "edit-isc": + cmdEditIsc(rest); + return; case "prune-isc": cmdPruneIsc(rest); return; diff --git a/test/project-cli.test.ts b/test/project-cli.test.ts index 11bb489..005142c 100644 --- a/test/project-cli.test.ts +++ b/test/project-cli.test.ts @@ -453,4 +453,80 @@ describe("project CLI", () => { expect(r.code).toBe(1); expect(r.stderr).toContain("ISC-99 not found"); }); + + test("edit-isc rewrites the text, keeping the id and open state", async () => { + await runCli(["create", "edit", "--path", "/tmp/edit-fake"]); + await runCli(["add-isc", "edit", "vague wording"]); + + const r = await runCli([ + "edit-isc", + "edit", + "1", + "sharp wording with a done-condition", + ]); + + expect(r.code).toBe(0); + expect(JSON.parse(r.stdout)).toEqual({ + edited: true, + id: 1, + status: "open", + previous: "vague wording", + text: "sharp wording with a done-condition", + }); + expect(section("edit", "Criteria")).toContain( + "- [ ] ISC-1: sharp wording with a done-condition" + ); + expect(section("edit", "Criteria")).not.toContain("vague wording"); + }); + + test("edit-isc keeps a closed ISC closed and in the Changelog", async () => { + await runCli(["create", "editc", "--path", "/tmp/editc-fake"]); + await runCli(["add-isc", "editc", "before"]); + await runCli(["complete-isc", "editc", "1"]); + + const got = JSON.parse((await runCli(["edit-isc", "editc", "1", "after"])).stdout); + + expect(got.status).toBe("closed"); + expect(section("editc", "Changelog")).toContain("- [x] ISC-1: after"); + expect(section("editc", "Criteria")).not.toContain("ISC-1"); + }); + + test("edit-isc leaves sibling ISCs untouched", async () => { + await runCli(["create", "editsib", "--path", "/tmp/editsib-fake"]); + await runCli(["add-isc", "editsib", "first"]); + await runCli(["add-isc", "editsib", "second"]); + + await runCli(["edit-isc", "editsib", "1", "rewritten"]); + + const criteria = section("editsib", "Criteria"); + expect(criteria).toContain("- [ ] ISC-1: rewritten"); + expect(criteria).toContain("- [ ] ISC-2: second"); + }); + + test("an edited id is still reserved against reuse", async () => { + await runCli(["create", "editres", "--path", "/tmp/editres-fake"]); + await runCli(["add-isc", "editres", "first"]); + await runCli(["edit-isc", "editres", "1", "first, reworded"]); + + const added = JSON.parse((await runCli(["add-isc", "editres", "second"])).stdout); + + expect(added.id).toBe(2); + }); + + test("edit-isc fails on an unknown id", async () => { + await runCli(["create", "editnone", "--path", "/tmp/editnone-fake"]); + const r = await runCli(["edit-isc", "editnone", "99", "nope"]); + expect(r.code).toBe(1); + expect(r.stderr).toContain("ISC-99 not found"); + }); + + test("edit-isc requires replacement text", async () => { + await runCli(["create", "editempty", "--path", "/tmp/editempty-fake"]); + await runCli(["add-isc", "editempty", "keep me"]); + + const r = await runCli(["edit-isc", "editempty", "1", " "]); + + expect(r.code).toBe(1); + expect(section("editempty", "Criteria")).toContain("keep me"); + }); }); From cfe1902b5b69b0ba43542121e0249cf40610ae5f Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 10:56:40 +0200 Subject: [PATCH 06/35] feat(project): add retire-isc for criteria that stopped being valid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - model an ISC as open, done, or retired instead of a checked flag - store a retired ISC as [~] under a Retired changelog heading - record the superseding ISC with --by - count a retired ISC as neither open nor done, and list it with --retired - reopen a retired ISC back into the open set - keep a retired id reserved in both the project and migrate id scans - report show-isc and list-isc status as open, done, or retired Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- assets/skills/projects/SKILL.md | 2 +- src/cli/migrate.ts | 2 +- src/tools/agent/project.ts | 116 ++++++++++++++++++++------ test/project-cli.test.ts | 139 ++++++++++++++++++++++++++++++-- 4 files changed, 227 insertions(+), 32 deletions(-) diff --git a/assets/skills/projects/SKILL.md b/assets/skills/projects/SKILL.md index 423d6d4..96dd331 100644 --- a/assets/skills/projects/SKILL.md +++ b/assets/skills/projects/SKILL.md @@ -48,7 +48,7 @@ The body of each ISA.md holds spec sections. Use `update-section` to set them: | Decisions | `decisions` | Auto-managed by `add-decision`; dated bullet list | | Changelog | `changelog` | Archive of completed ISCs (`complete-isc` moves them here under a dated `### Archived` heading) plus any milestone notes | -**ISC archive model.** `complete-isc` does not just check a box — it moves the ISC line out of Criteria and into the Changelog archive, so Criteria always reflects exactly the open work and never bloats the context loaded at session start. `list-isc <name>` shows open ISCs by default; pass `--closed` (archived only) or `--all` (open + archived) to read finished ones. `reopen-isc` pulls an archived ISC back into the open set. `prune-isc <name>` backfills legacy projects by sweeping any done ISCs still sitting in Criteria into the archive in one pass. +**ISC archive model.** `complete-isc` does not just check a box — it moves the ISC line out of Criteria and into the Changelog archive, so Criteria always reflects exactly the open work and never bloats the context loaded at session start. `list-isc <name>` shows open ISCs by default; pass `--closed` (archived only) or `--all` (open + archived) to read finished ones. `reopen-isc` pulls an archived or retired ISC back into the open set. `edit-isc <name> <id> "new text"` rewrites an ISC's wording in place, keeping its id and state — use it to sharpen a vague criterion instead of closing and refiling. `retire-isc <name> <id> [--by <id>]` closes an ISC that stopped being valid: it files under a `### Retired` heading as `[~]` rather than claiming the work was done, and `--by` records the superseding ISC. A retired ISC counts as neither open nor done, is listed with `--retired`, and keeps its id reserved so no future ISC can reuse it. `prune-isc <name>` backfills legacy projects by sweeping any done ISCs still sitting in Criteria into the archive in one pass. ## Routing diff --git a/src/cli/migrate.ts b/src/cli/migrate.ts index 8cdece8..f8cc886 100644 --- a/src/cli/migrate.ts +++ b/src/cli/migrate.ts @@ -105,7 +105,7 @@ const v1Projects: Migration = { function nextIscId(criteria: string): number { const ids: number[] = []; for (const line of criteria.split("\n")) { - const m = new RegExp(/^-\s+\[[ x]\]\s+ISC-(\d+):/i).exec(line); + const m = new RegExp(/^-\s+\[[ x~]\]\s+ISC-(\d+):/i).exec(line); if (m) ids.push(Number(m[1])); } return ids.length > 0 ? Math.max(...ids) + 1 : 1; diff --git a/src/tools/agent/project.ts b/src/tools/agent/project.ts index 1fee133..6d1bc31 100644 --- a/src/tools/agent/project.ts +++ b/src/tools/agent/project.ts @@ -132,13 +132,16 @@ function cmdResume(args: string[]): void { if (!name) fail("Usage: resume <name>"); const { criteria, changelog, ...project } = requireProject(name); const iscs = parseIscs(criteria ?? ""); - const openIscs = iscs.filter((i) => !i.checked); - const done = iscs.filter((i) => i.checked).length + parseIscs(changelog ?? "").length; + const archived = parseIscs(changelog ?? ""); + const openIscs = iscs.filter((i) => i.status === "open"); + const all = [...iscs, ...archived]; + const done = all.filter((i) => i.status === "done").length; + const retired = all.filter((i) => i.status === "retired").length; ok({ project: { ...project, open_iscs: openIscs.map((i) => ({ id: i.id, title: iscTitle(i.text) })), - isc_summary: { open: openIscs.length, done }, + isc_summary: { open: openIscs.length, done, retired }, }, }); } @@ -350,17 +353,34 @@ function cmdRm(args: string[]): void { // ── ISC helpers ────────────────────────────────────────────────── +// Three states, not two: a retired ISC is one that stopped being valid, which the +// record must not report as completed work. The box character is the storage form +// and the id stays in it, so a retired line keeps reserving its id in nextIscId. +type IscStatus = "open" | "done" | "retired"; + +const ISC_BOX: Record<IscStatus, string> = { + open: "[ ]", + done: "[x]", + retired: "[~]", +}; + +function statusFromBox(box: string): IscStatus { + if (box.toLowerCase() === "x") return "done"; + if (box === "~") return "retired"; + return "open"; +} + interface Isc { id: number; text: string; - checked: boolean; + status: IscStatus; } function parseIscs(criteria: string): Isc[] { const out: Isc[] = []; for (const line of criteria.split("\n")) { - const m = new RegExp(/^-\s+\[( |x)\]\s+ISC-(\d+):\s+(.+)$/i).exec(line); - if (m) out.push({ id: Number(m[2]), text: m[3].trim(), checked: m[1] === "x" }); + const m = new RegExp(/^-\s+\[( |x|~)\]\s+ISC-(\d+):\s+(.+)$/i).exec(line); + if (m) out.push({ id: Number(m[2]), text: m[3].trim(), status: statusFromBox(m[1]) }); } return out; } @@ -387,7 +407,7 @@ function removeIscLine( ): { line: string | null; rest: string } { const lines = section.split("\n"); const idx = lines.findIndex((l) => - new RegExp(String.raw`^-\s+\[[ x]\]\s+ISC-${id}:`).test(l) + new RegExp(String.raw`^-\s+\[[ x~]\]\s+ISC-${id}:`).test(l) ); if (idx === -1) return { line: null, rest: section }; const [line] = lines.splice(idx, 1); @@ -417,8 +437,12 @@ function dropEmptyArchiveHeadings(changelog: string): string { .trim(); } -function archiveLine(changelog: string | undefined, doneLine: string): string { - const heading = `### Archived ${new Date().toISOString().slice(0, 10)}`; +function archiveLine( + changelog: string | undefined, + doneLine: string, + kind: "Archived" | "Retired" = "Archived" +): string { + const heading = `### ${kind} ${new Date().toISOString().slice(0, 10)}`; const base = (changelog ?? "").trim(); if (base.includes(heading)) return `${base}\n${doneLine}`; return base ? `${base}\n\n${heading}\n${doneLine}` : `${heading}\n${doneLine}`; @@ -472,7 +496,7 @@ function cmdReopenIsc(args: string[]): void { const id = Number(args[1] ?? fail("Usage: reopen-isc <name> <id>")); if (!Number.isInteger(id) || id < 1) fail("ISC id must be a positive integer"); const p = requireProject(name); - if (parseIscs(p.criteria ?? "").some((i) => i.id === id && !i.checked)) { + if (parseIscs(p.criteria ?? "").some((i) => i.id === id && i.status === "open")) { ok({ checked: false, id, alreadyOpen: true }); return; } @@ -484,16 +508,17 @@ function cmdReopenIsc(args: string[]): void { if (removed.line) p.criteria = removed.rest; } if (!removed.line) fail(`ISC-${id} not found in project "${name}"`); - const openLine = removed.line.replace(/\[x\]/i, "[ ]"); + const openLine = removed.line.replace(/\[[x~]\]/i, "[ ]"); p.criteria = p.criteria ? `${p.criteria.trimEnd()}\n${openLine}` : openLine; p.updated = now(); writeProject(p); ok({ checked: false, id }); } -function selectIscs(open: Isc[], done: Isc[], flags: Set<string>): Isc[] { - if (flags.has("--all")) return [...open, ...done]; +function selectIscs(open: Isc[], done: Isc[], retired: Isc[], flags: Set<string>): Isc[] { + if (flags.has("--all")) return [...open, ...done, ...retired]; if (flags.has("--closed")) return done; + if (flags.has("--retired")) return retired; return open; } @@ -501,17 +526,20 @@ function cmdListIsc(args: string[]): void { const flags = new Set(args.filter((a) => a.startsWith("--"))); const name = args.find((a) => !a.startsWith("--")) ?? - fail("Usage: list-isc <name> [--all | --closed]"); + fail("Usage: list-isc <name> [--all | --closed | --retired]"); const p = requireProject(name); const criteria = parseIscs(p.criteria ?? ""); - const open = criteria.filter((i) => !i.checked); - const done = [...criteria.filter((i) => i.checked), ...parseIscs(p.changelog ?? "")]; + const all = [...criteria, ...parseIscs(p.changelog ?? "")]; + const open = all.filter((i) => i.status === "open"); + const done = all.filter((i) => i.status === "done"); + const retired = all.filter((i) => i.status === "retired"); ok({ name, - total: open.length + done.length, + total: open.length + done.length + retired.length, open: open.length, done: done.length, - iscs: selectIscs(open, done, flags), + retired: retired.length, + iscs: selectIscs(open, done, retired, flags), }); } @@ -526,7 +554,41 @@ function cmdShowIsc(args: string[]): void { (i) => i.id === id ); if (!isc) fail(`ISC-${id} not found in project "${name}".`); - ok({ name, id: isc.id, status: isc.checked ? "closed" : "open", text: isc.text }); + ok({ name, id: isc.id, status: isc.status, text: isc.text }); +} + +// retire-isc closes an ISC that stopped being valid, which complete-isc cannot say: +// completing files it as done work. The line moves to the Changelog under its own +// heading as [~], so it still reserves its id and never reads as finished. +function cmdRetireIsc(args: string[]): void { + const positional = args.filter((a) => !a.startsWith("--")); + const name = positional[0]; + const id = Number(positional[1]); + if (!name || !Number.isInteger(id) || id < 1) { + fail("Usage: retire-isc <name> <id> [--by <supersedingId>]"); + } + const byIndex = args.indexOf("--by"); + const by = byIndex === -1 ? null : Number(args[byIndex + 1]); + if (byIndex !== -1 && (!Number.isInteger(by) || (by ?? 0) < 1)) { + fail("--by expects a positive ISC id"); + } + const p = requireProject(name); + if (parseIscs(p.changelog ?? "").some((i) => i.id === id && i.status === "retired")) { + ok({ retired: true, id, alreadyRetired: true }); + return; + } + const { line, rest } = removeIscLine(p.criteria ?? "", id); + if (!line) fail(`ISC-${id} not found in project "${name}"`); + const suffix = by ? ` (superseded by ISC-${by})` : ""; + p.criteria = rest; + p.changelog = archiveLine( + p.changelog, + `${line.replace(/\[[ x]\]/i, "[~]")}${suffix}`, + "Retired" + ); + p.updated = now(); + writeProject(p); + ok({ retired: true, id, supersededBy: by, archived: true }); } // edit-isc rewrites one ISC's text in place, keeping its id and open/done state. @@ -544,12 +606,12 @@ function cmdEditIsc(args: string[]): void { const isc = inCriteria ?? parseIscs(p.changelog ?? "").find((i) => i.id === id); if (!isc) fail(`ISC-${id} not found in project "${name}".`); - const box = isc.checked ? "[x]" : "[ ]"; + const box = ISC_BOX[isc.status]; const rewrite = (section: string) => section .split("\n") .map((l) => - new RegExp(String.raw`^-\s+\[[ x]\]\s+ISC-${id}:`, "i").test(l) + new RegExp(String.raw`^-\s+\[[ x~]\]\s+ISC-${id}:`, "i").test(l) ? `- ${box} ISC-${id}: ${text}` : l ) @@ -562,7 +624,7 @@ function cmdEditIsc(args: string[]): void { ok({ edited: true, id, - status: isc.checked ? "closed" : "open", + status: isc.status, previous: isc.text, text, }); @@ -573,7 +635,7 @@ function cmdEditIsc(args: string[]): void { function cmdPruneIsc(args: string[]): void { const name = args[0] ?? fail("Usage: prune-isc <name>"); const p = requireProject(name); - const done = parseIscs(p.criteria ?? "").filter((i) => i.checked); + const done = parseIscs(p.criteria ?? "").filter((i) => i.status !== "open"); for (const isc of done) { const { line, rest } = removeIscLine(p.criteria ?? "", isc.id); if (!line) continue; @@ -584,7 +646,7 @@ function cmdPruneIsc(args: string[]): void { p.updated = now(); writeProject(p); } - const openLeft = parseIscs(p.criteria ?? "").filter((i) => !i.checked).length; + const openLeft = parseIscs(p.criteria ?? "").filter((i) => i.status === "open").length; ok({ pruned: done.length, name, remaining_open: openLeft }); } @@ -667,9 +729,10 @@ Commands: add-isc <name> "title" append a new open ISC to Criteria complete-isc <name> <id> mark ISC-N as done reopen-isc <name> <id> reopen ISC-N (mark not done) - list-isc <name> [--all | --closed] list open ISCs (default); --all or --closed for done + list-isc <name> [--all | --closed | --retired] list open ISCs (default); --all, --closed, or --retired show-isc <name> <id> print one ISC's full text edit-isc <name> <id> "new text" rewrite ISC-N's text, keeping its id and state + retire-isc <name> <id> [--by <id>] close ISC-N as no longer valid, not as done prune-isc <name> archive done ISCs from Criteria into the Changelog isa-init <name> mark project as ISA-initialized scaffold-task-isa <title> create a one-shot task ISA in memory/work/ @@ -765,6 +828,9 @@ function run(): void { case "show-isc": cmdShowIsc(rest); return; + case "retire-isc": + cmdRetireIsc(rest); + return; case "edit-isc": cmdEditIsc(rest); return; diff --git a/test/project-cli.test.ts b/test/project-cli.test.ts index 005142c..d078b93 100644 --- a/test/project-cli.test.ts +++ b/test/project-cli.test.ts @@ -307,7 +307,7 @@ describe("project CLI", () => { expect(def.done).toBe(1); expect(def.iscs).toHaveLength(1); expect(def.iscs[0].id).toBe(2); - expect(def.iscs.every((i: { checked: boolean }) => !i.checked)).toBe(true); + expect(def.iscs.every((i: { status: string }) => i.status === "open")).toBe(true); const all = JSON.parse((await runCli(["list-isc", "iscproj", "--all"])).stdout); expect(all.iscs).toHaveLength(2); @@ -315,7 +315,7 @@ describe("project CLI", () => { const closed = JSON.parse((await runCli(["list-isc", "iscproj", "--closed"])).stdout); expect(closed.iscs).toHaveLength(1); expect(closed.iscs[0].id).toBe(1); - expect(closed.iscs[0].checked).toBe(true); + expect(closed.iscs[0].status).toBe("done"); }); test("complete-isc moves the line out of Criteria into the Changelog", async () => { @@ -410,7 +410,7 @@ describe("project CLI", () => { expect(project.changelog).toBeUndefined(); // Open ISCs surface as {id, title}; closed ones only as a count. expect(project.open_iscs).toEqual([{ id: 1, title: "first open thing" }]); - expect(project.isc_summary).toEqual({ open: 1, done: 1 }); + expect(project.isc_summary).toEqual({ open: 1, done: 1, retired: 0 }); }); test("resume truncates a long ISC line to a glanceable title", async () => { @@ -443,7 +443,7 @@ describe("project CLI", () => { await runCli(["add-isc", "showc", "will be done"]); await runCli(["complete-isc", "showc", "1"]); const got = JSON.parse((await runCli(["show-isc", "showc", "1"])).stdout); - expect(got.status).toBe("closed"); + expect(got.status).toBe("done"); expect(got.text).toBe("will be done"); }); @@ -486,7 +486,7 @@ describe("project CLI", () => { const got = JSON.parse((await runCli(["edit-isc", "editc", "1", "after"])).stdout); - expect(got.status).toBe("closed"); + expect(got.status).toBe("done"); expect(section("editc", "Changelog")).toContain("- [x] ISC-1: after"); expect(section("editc", "Criteria")).not.toContain("ISC-1"); }); @@ -529,4 +529,133 @@ describe("project CLI", () => { expect(r.code).toBe(1); expect(section("editempty", "Criteria")).toContain("keep me"); }); + + test("retire-isc files the ISC under Retired, not Archived", async () => { + await runCli(["create", "ret", "--path", "/tmp/ret-fake"]); + await runCli(["add-isc", "ret", "no longer valid"]); + + const r = await runCli(["retire-isc", "ret", "1"]); + + expect(r.code).toBe(0); + const changelog = section("ret", "Changelog"); + expect(changelog).toContain("### Retired"); + expect(changelog).toContain("- [~] ISC-1: no longer valid"); + expect(changelog).not.toContain("### Archived"); + expect(section("ret", "Criteria")).not.toContain("ISC-1"); + }); + + test("retire-isc records the superseding id with --by", async () => { + await runCli(["create", "retby", "--path", "/tmp/retby-fake"]); + await runCli(["add-isc", "retby", "old framing"]); + await runCli(["add-isc", "retby", "new framing"]); + + const got = JSON.parse( + (await runCli(["retire-isc", "retby", "1", "--by", "2"])).stdout + ); + + expect(got.supersededBy).toBe(2); + expect(section("retby", "Changelog")).toContain("(superseded by ISC-2)"); + }); + + test("a retired id is never reused by add-isc", async () => { + await runCli(["create", "retres", "--path", "/tmp/retres-fake"]); + await runCli(["add-isc", "retres", "first"]); + await runCli(["retire-isc", "retres", "1"]); + + const added = JSON.parse((await runCli(["add-isc", "retres", "second"])).stdout); + + expect(added.id).toBe(2); + }); + + test("a retired ISC counts as neither open nor done", async () => { + await runCli(["create", "retcount", "--path", "/tmp/retcount-fake"]); + await runCli(["add-isc", "retcount", "stays open"]); + await runCli(["add-isc", "retcount", "gets done"]); + await runCli(["add-isc", "retcount", "gets retired"]); + await runCli(["complete-isc", "retcount", "2"]); + await runCli(["retire-isc", "retcount", "3"]); + + const listed = JSON.parse((await runCli(["list-isc", "retcount"])).stdout); + + expect(listed.open).toBe(1); + expect(listed.done).toBe(1); + expect(listed.retired).toBe(1); + expect(listed.iscs.map((i: { id: number }) => i.id)).toEqual([1]); + }); + + test("resume excludes retired ISCs from open work", async () => { + await runCli(["create", "retres2", "--path", "/tmp/retres2-fake"]); + await runCli(["add-isc", "retres2", "live work"]); + await runCli(["add-isc", "retres2", "dead work"]); + await runCli(["retire-isc", "retres2", "2"]); + + const got = JSON.parse((await runCli(["resume", "retres2"])).stdout); + + expect(got.project.open_iscs.map((i: { id: number }) => i.id)).toEqual([1]); + expect(got.project.isc_summary).toEqual({ open: 1, done: 0, retired: 1 }); + }); + + test("list-isc --retired returns only retired, --all includes them", async () => { + await runCli(["create", "retflag", "--path", "/tmp/retflag-fake"]); + await runCli(["add-isc", "retflag", "open one"]); + await runCli(["add-isc", "retflag", "retired one"]); + await runCli(["retire-isc", "retflag", "2"]); + + const only = JSON.parse((await runCli(["list-isc", "retflag", "--retired"])).stdout); + const all = JSON.parse((await runCli(["list-isc", "retflag", "--all"])).stdout); + + expect(only.iscs.map((i: { id: number }) => i.id)).toEqual([2]); + expect(all.iscs.map((i: { id: number }) => i.id).sort()).toEqual([1, 2]); + }); + + test("show-isc reports retired status", async () => { + await runCli(["create", "retshow", "--path", "/tmp/retshow-fake"]); + await runCli(["add-isc", "retshow", "gone"]); + await runCli(["retire-isc", "retshow", "1"]); + + const got = JSON.parse((await runCli(["show-isc", "retshow", "1"])).stdout); + + expect(got.status).toBe("retired"); + expect(got.text).toBe("gone"); + }); + + test("reopen-isc brings a retired ISC back to open", async () => { + await runCli(["create", "retreopen", "--path", "/tmp/retreopen-fake"]); + await runCli(["add-isc", "retreopen", "back from the dead"]); + await runCli(["retire-isc", "retreopen", "1"]); + + await runCli(["reopen-isc", "retreopen", "1"]); + + expect(section("retreopen", "Criteria")).toContain("- [ ] ISC-1: back from the dead"); + expect(section("retreopen", "Changelog")).not.toContain("ISC-1"); + }); + + test("edit-isc preserves the retired state", async () => { + await runCli(["create", "retedit", "--path", "/tmp/retedit-fake"]); + await runCli(["add-isc", "retedit", "before"]); + await runCli(["retire-isc", "retedit", "1"]); + + const got = JSON.parse((await runCli(["edit-isc", "retedit", "1", "after"])).stdout); + + expect(got.status).toBe("retired"); + expect(section("retedit", "Changelog")).toContain("- [~] ISC-1: after"); + }); + + test("retire-isc fails on an unknown id", async () => { + await runCli(["create", "retnone", "--path", "/tmp/retnone-fake"]); + const r = await runCli(["retire-isc", "retnone", "99"]); + expect(r.code).toBe(1); + expect(r.stderr).toContain("ISC-99 not found"); + }); + + test("complete-isc still archives as done, unaffected by retire", async () => { + await runCli(["create", "retdone", "--path", "/tmp/retdone-fake"]); + await runCli(["add-isc", "retdone", "genuinely finished"]); + await runCli(["complete-isc", "retdone", "1"]); + + const changelog = section("retdone", "Changelog"); + expect(changelog).toContain("### Archived"); + expect(changelog).toContain("- [x] ISC-1: genuinely finished"); + expect(changelog).not.toContain("[~]"); + }); }); From a2eef513a7adfc9abef1db8eeb0134d2d8c1eb39 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 11:02:17 +0200 Subject: [PATCH 07/35] chore(biome): exclude the install smoke-test home from the check scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - skip .test-install-home, whose skill symlinks resolve only while the suite runs Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- biome.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 249f983..b783cea 100644 --- a/biome.json +++ b/biome.json @@ -24,7 +24,8 @@ "!telos", "klint.rules.ts", "stryker.config.mjs", - "!.stryker-tmp" + "!.stryker-tmp", + "!.test-install-home" ] }, "formatter": { From 4b0d75ba302d1fa9ac4bf073c54ed58af163f134 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 11:07:11 +0200 Subject: [PATCH 08/35] perf(hooks): skip the session-end gates on a clean worktree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - report success with the skip reason when git status is empty - treat an unreadable git status as changed so the gates still run - count untracked files as changes Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .agents/hooks/run-hook.ts | 31 ++++++++-- test/run-hook-skip.test.ts | 115 +++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 test/run-hook-skip.test.ts diff --git a/.agents/hooks/run-hook.ts b/.agents/hooks/run-hook.ts index 37208f6..a2bc877 100644 --- a/.agents/hooks/run-hook.ts +++ b/.agents/hooks/run-hook.ts @@ -17,6 +17,25 @@ function writeFailure(format: HookFormat, output: string): void { process.stderr.write(output); } +function writeSuccess(format: HookFormat, output: string): number { + if (format === "codex") return 0; + process.stdout.write(JSON.stringify({ output })); + return 0; +} + +// Fails closed: a non-zero git status (not a repo, git missing, index locked) +// reports changes so the gates still run. Only a confirmed-empty status skips. +// --porcelain=v1 lists untracked files too, so a new file counts as a change. +function worktreeHasChanges(): boolean { + const r = spawnSync("git status --porcelain=v1", { + encoding: "utf8", + shell: true, + stdio: ["ignore", "pipe", "pipe"], + }); + if ((r.status ?? -1) !== 0) return true; + return (r.stdout ?? "").trim().length > 0; +} + // Helper for agent hook scripts. Each hook file (lint.ts, test.ts, ...) is a // thin wrapper that calls runHook(["bun", "run", "<script>"]). We capture // stdout+stderr, return them on success in a JSON envelope (so Claude/opencode @@ -28,6 +47,12 @@ export function runHook(args: string[], format = hookFormatFromArgs()): number { writeFailure(format, "run-hook: no command provided"); return 2; } + if (!worktreeHasChanges()) { + return writeSuccess( + format, + "skipped: worktree clean, HEAD already gated by pre-commit and CI" + ); + } const command = args.join(" "); const r = spawnSync(command, { encoding: "utf8", @@ -38,11 +63,7 @@ export function runHook(args: string[], format = hookFormatFromArgs()): number { const output = out || "(no output)"; const ok = (r.status ?? -1) === 0; - if (ok) { - if (format === "codex") return 0; - process.stdout.write(JSON.stringify({ output: "ok" })); - return 0; - } + if (ok) return writeSuccess(format, "ok"); writeFailure(format, output); return 2; } diff --git a/test/run-hook-skip.test.ts b/test/run-hook-skip.test.ts new file mode 100644 index 0000000..7a6db25 --- /dev/null +++ b/test/run-hook-skip.test.ts @@ -0,0 +1,115 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; + +const ROOT = resolve(import.meta.dir, "../.test-tmp/run-hook-skip"); +const HOOK = resolve(import.meta.dir, "../.agents/hooks/run-hook.ts"); +const MARKER = "gate-ran"; + +function git(cwd: string, ...args: string[]) { + spawnSync("git", args, { cwd, stdio: "ignore" }); +} + +/** A repo whose only commit is one tracked file, so the worktree starts clean. */ +function makeRepo(name: string): string { + const dir = resolve(ROOT, name); + mkdirSync(dir, { recursive: true }); + git(dir, "init", "-q"); + git(dir, "config", "user.email", "t@example.com"); + git(dir, "config", "user.name", "t"); + writeFileSync(resolve(dir, "tracked.txt"), "one\n"); + git(dir, "add", "-A"); + git(dir, "commit", "-qm", "init"); + return dir; +} + +/** Runs the hook with a command that prints MARKER, so its absence proves a skip. */ +function runHookIn(cwd: string) { + const r = spawnSync("bun", ["run", HOOK, "echo", MARKER], { + cwd, + encoding: "utf-8", + }); + return { code: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" }; +} + +beforeAll(() => { + if (existsSync(ROOT)) rmSync(ROOT, { recursive: true }); + mkdirSync(ROOT, { recursive: true }); +}); + +afterAll(() => { + if (existsSync(ROOT)) rmSync(ROOT, { recursive: true }); +}); + +describe("run-hook clean-worktree skip", () => { + test("skips the gate on a clean worktree and says why", () => { + const r = runHookIn(makeRepo("clean")); + + expect(r.code).toBe(0); + expect(JSON.parse(r.stdout).output).toBe( + "skipped: worktree clean, HEAD already gated by pre-commit and CI" + ); + }); + + test("runs the gate when a tracked file is modified", () => { + const dir = makeRepo("modified"); + writeFileSync(resolve(dir, "tracked.txt"), "changed\n"); + + const r = runHookIn(dir); + + expect(r.code).toBe(0); + expect(JSON.parse(r.stdout).output).toBe("ok"); + }); + + test("runs the gate when only an untracked file exists", () => { + const dir = makeRepo("untracked"); + writeFileSync(resolve(dir, "brand-new.txt"), "new\n"); + + const r = runHookIn(dir); + + expect(r.code).toBe(0); + expect(JSON.parse(r.stdout).output).toBe("ok"); + }); + + // Must live outside the repo tree: a directory nested inside it would make + // `git status` walk up and succeed against the parent, so the gate would run + // for the wrong reason and this case would prove nothing. + test("runs the gate outside a git repository", () => { + const dir = mkdtempSync(resolve(tmpdir(), "pal-run-hook-")); + try { + expect(spawnSync("git", ["status"], { cwd: dir }).status).not.toBe(0); + + const r = runHookIn(dir); + + expect(r.code).toBe(0); + expect(JSON.parse(r.stdout).output).toBe("ok"); + } finally { + rmSync(dir, { recursive: true }); + } + }); + + test("a failing gate still blocks when the worktree is dirty", () => { + const dir = makeRepo("failing"); + writeFileSync(resolve(dir, "tracked.txt"), "changed\n"); + + const r = spawnSync("bun", ["run", HOOK, "exit", "3"], { + cwd: dir, + encoding: "utf-8", + }); + + expect(r.status).toBe(2); + }); + + test("a clean worktree skips a gate that would otherwise fail", () => { + const dir = makeRepo("clean-failing"); + + const r = spawnSync("bun", ["run", HOOK, "exit", "3"], { + cwd: dir, + encoding: "utf-8", + }); + + expect(r.status).toBe(0); + }); +}); From e93ab59f8a2013f3295b5734469bb063ffb52295 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 11:20:52 +0200 Subject: [PATCH 09/35] chore: add secretlint to the gate chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scan the tree with the recommended rule preset - run it at session end in every agent, on pre-commit, and in CI - skip lockfiles, archives, and generated test homes Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .agents/hooks/secretlint.ts | 4 ++ .claude/settings.json | 29 +++++++-- .codex/hooks.json | 8 +++ .cursor/hooks.json | 23 +++++-- .github/workflows/ci.yml | 3 + .husky/pre-commit | 2 +- .opencode/plugins/lint.ts | 1 + .secretlintignore | 9 +++ .secretlintrc.json | 3 + bun.lock | 124 +++++++++++++++++++++++++++++++----- knip.json | 3 +- package.json | 3 + 12 files changed, 185 insertions(+), 27 deletions(-) create mode 100644 .agents/hooks/secretlint.ts create mode 100644 .secretlintignore create mode 100644 .secretlintrc.json diff --git a/.agents/hooks/secretlint.ts b/.agents/hooks/secretlint.ts new file mode 100644 index 0000000..0bb3352 --- /dev/null +++ b/.agents/hooks/secretlint.ts @@ -0,0 +1,4 @@ +import { runHook } from "./run-hook"; + +const exitCode = runHook(["bun", "run", "secretlint"]); +process.exit(exitCode); diff --git a/.claude/settings.json b/.claude/settings.json index 00d5676..93d304f 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,11 +4,30 @@ "Stop": [ { "hooks": [ - { "type": "command", "command": "bun .agents/hooks/check.ts" }, - { "type": "command", "command": "bun .agents/hooks/type-check.ts" }, - { "type": "command", "command": "bun .agents/hooks/knip.ts" }, - { "type": "command", "command": "bun .agents/hooks/jscpd.ts" }, - { "type": "command", "command": "bun .agents/hooks/klint.ts" } + { + "type": "command", + "command": "bun .agents/hooks/check.ts" + }, + { + "type": "command", + "command": "bun .agents/hooks/type-check.ts" + }, + { + "type": "command", + "command": "bun .agents/hooks/knip.ts" + }, + { + "type": "command", + "command": "bun .agents/hooks/jscpd.ts" + }, + { + "type": "command", + "command": "bun .agents/hooks/klint.ts" + }, + { + "type": "command", + "command": "bun .agents/hooks/secretlint.ts" + } ] } ] diff --git a/.codex/hooks.json b/.codex/hooks.json index 47d5feb..9e24538 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -41,6 +41,14 @@ "command": "bun .agents/hooks/klint.ts --codex" } ] + }, + { + "hooks": [ + { + "type": "command", + "command": "bun .agents/hooks/secretlint.ts --codex" + } + ] } ] } diff --git a/.cursor/hooks.json b/.cursor/hooks.json index 664ae02..7c633a1 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -2,11 +2,24 @@ "version": 1, "hooks": { "stop": [ - { "command": "bun .agents/hooks/check.ts" }, - { "command": "bun .agents/hooks/type-check.ts" }, - { "command": "bun .agents/hooks/knip.ts" }, - { "command": "bun .agents/hooks/jscpd.ts" }, - { "command": "bun .agents/hooks/klint.ts" } + { + "command": "bun .agents/hooks/check.ts" + }, + { + "command": "bun .agents/hooks/type-check.ts" + }, + { + "command": "bun .agents/hooks/knip.ts" + }, + { + "command": "bun .agents/hooks/jscpd.ts" + }, + { + "command": "bun .agents/hooks/klint.ts" + }, + { + "command": "bun .agents/hooks/secretlint.ts" + } ] } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4820ca3..228b7d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: - name: Klint run: bun run klint + - name: Secretlint + run: bun run secretlint + - name: Test run: bun run test diff --git a/.husky/pre-commit b/.husky/pre-commit index 03b88fc..fcba857 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -bun run lint-staged && bun run type-check && bun run knip && bun run jscpd && bun run klint +bun run lint-staged && bun run type-check && bun run knip && bun run jscpd && bun run klint && bun run secretlint diff --git a/.opencode/plugins/lint.ts b/.opencode/plugins/lint.ts index 212bbbd..dd15f81 100644 --- a/.opencode/plugins/lint.ts +++ b/.opencode/plugins/lint.ts @@ -11,6 +11,7 @@ export const LintPlugin: Plugin = async ({ $ }) => { await $`bun run knip`; await $`bun run jscpd`; await $`bun klint/cli.ts --json`; + await $`bun run secretlint`; }, }; }; diff --git a/.secretlintignore b/.secretlintignore new file mode 100644 index 0000000..dc69085 --- /dev/null +++ b/.secretlintignore @@ -0,0 +1,9 @@ +node_modules +bun.lock +backups +pal-export-*.zip +.test-home* +.test-install-home +.test-tmp +.stryker-tmp +reports diff --git a/.secretlintrc.json b/.secretlintrc.json new file mode 100644 index 0000000..3a6a79a --- /dev/null +++ b/.secretlintrc.json @@ -0,0 +1,3 @@ +{ + "rules": [{ "id": "@secretlint/secretlint-rule-preset-recommend" }] +} diff --git a/bun.lock b/bun.lock index 0785a66..aab5593 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "@hughescr/stryker-bun-runner": "1.3.8", "@konvert7/klint": "0.20.0", "@opencode-ai/plugin": "latest", + "@secretlint/secretlint-rule-preset-recommend": "13.0.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.8", @@ -31,6 +32,7 @@ "knip": "^6.14.1", "lint-staged": "17.0.5", "promptfoo": "0.121.14", + "secretlint": "13.0.2", "semantic-release": "^25.0.3", "typescript": "^5.9.3", }, @@ -149,6 +151,10 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@azu/format-text": ["@azu/format-text@1.0.2", "", {}, "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg=="], + + "@azu/style-format": ["@azu/style-format@1.0.1", "", { "dependencies": { "@azu/format-text": "^1.0.1" } }, "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g=="], + "@azure-rest/core-client": ["@azure-rest/core-client@2.6.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ=="], "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], @@ -803,6 +809,28 @@ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + "@secretlint/config-creator": ["@secretlint/config-creator@13.0.2", "", { "dependencies": { "@secretlint/types": "13.0.2" } }, "sha512-wEHE/x7jeaWnDqVhzUg3qLsnl3NOQTA2fVnxwc0Z7dn3SOSh5sEKFr6xTp/LhZxyuHnKJltlcF085UDUFOoqYQ=="], + + "@secretlint/config-loader": ["@secretlint/config-loader@13.0.2", "", { "dependencies": { "@secretlint/profiler": "13.0.2", "@secretlint/resolver": "13.0.2", "@secretlint/types": "13.0.2", "ajv": "^8.20.0", "debug": "^4.4.3", "rc-config-loader": "^4.1.4" } }, "sha512-r0ZVJ3oGUwWqoFPwhR9RK/fnp358TImaa72UhrBdBiuzMvAcPtE6EbnAYhbj0Yr5QS6ns+GI11jV344MLqkhTg=="], + + "@secretlint/core": ["@secretlint/core@13.0.2", "", { "dependencies": { "@secretlint/profiler": "13.0.2", "@secretlint/types": "13.0.2", "debug": "^4.4.3", "structured-source": "^4.0.0" } }, "sha512-15vg+zCmjQuDFVoOPNt0TyEu0Z95eir2MO19++wG82yM/vbPtFu/m9qd6oh54ZOBXgMcchdGDTxMwJNDnCLxcA=="], + + "@secretlint/formatter": ["@secretlint/formatter@13.0.2", "", { "dependencies": { "@secretlint/resolver": "13.0.2", "@secretlint/types": "13.0.2", "@textlint/linter-formatter": "^15.6.1", "@textlint/module-interop": "^15.6.1", "@textlint/types": "^15.6.1", "chalk": "^5.6.2", "debug": "^4.4.3", "pluralize": "^8.0.0", "strip-ansi": "^7.2.0", "table": "^6.9.0", "terminal-link": "^5.0.0" } }, "sha512-DoU+aEsqPQxiWTQBNkTvQDmGTWB+TQS4UC6XEliWYv4KHpMV81O52jOE7XKS7QUoDMD4dbjh5wUTAgNO1dpzEg=="], + + "@secretlint/node": ["@secretlint/node@13.0.2", "", { "dependencies": { "@secretlint/config-loader": "13.0.2", "@secretlint/core": "13.0.2", "@secretlint/formatter": "13.0.2", "@secretlint/profiler": "13.0.2", "@secretlint/source-creator": "13.0.2", "@secretlint/types": "13.0.2", "debug": "^4.4.3", "p-map": "^7.0.4" } }, "sha512-xAK+0INjjMASH5guHLSozl++6kDyeOTy4FNC6dhPA7ieebCmHe/+QiAsr4KP9yDRZoh/uRmEno2NMDy5lgQpkA=="], + + "@secretlint/profiler": ["@secretlint/profiler@13.0.2", "", {}, "sha512-vGaLZCoi9KewOF+sM0iIEBviWaH9mls1HmQFBgEPq4fnvmVO4PfKIkVr4CcAFm3Msg4NjzFE2RBAGwRR+5HEmQ=="], + + "@secretlint/resolver": ["@secretlint/resolver@13.0.2", "", {}, "sha512-Ko50V0P3YAtEO20xBTczOzguBVk6+taSqkoAPHoOSdsTsSq1gzeYggY1UC8vVEHPGyRp/mb2QOuwIShdJZ8JFg=="], + + "@secretlint/secretlint-rule-preset-recommend": ["@secretlint/secretlint-rule-preset-recommend@13.0.2", "", {}, "sha512-D03Kw51qOgffj9zNlJFZUarVmP8zGcCZNi7A14+vZtHskbbBPEsS/f09idb48rrlMSaRMBN29IkqfbmPyL9xtw=="], + + "@secretlint/source-creator": ["@secretlint/source-creator@13.0.2", "", { "dependencies": { "@secretlint/types": "13.0.2", "istextorbinary": "^9.5.0" } }, "sha512-tSooHad8QL+lqHP88zH9F8GMoR7bHUqPja5M/QnNHGnvOq/8OT1V8t1uigm6jlD03oJWNLwwF8QeHIedWKJ13g=="], + + "@secretlint/types": ["@secretlint/types@13.0.2", "", {}, "sha512-cAZjr6ZTKgSuJMLyTGDrUEtK3XEZa+JStIkD+DmdkT32VGAN+dQJiYr1So3XJopsKSrqhP5kjZKT8WWtftZIpQ=="], + + "@secretlint/walker": ["@secretlint/walker@13.0.2", "", { "dependencies": { "ignore": "^7.0.5", "picomatch": "^4.0.4" } }, "sha512-9GtWeTb763Ilr/nbRzdFidfZTBtbNEsz/BKrilgqLpKR9EqSWe9eYWDQaLIYb4PddIhwOgHuahRrX2BYATNcLQ=="], + "@semantic-release/changelog": ["@semantic-release/changelog@6.0.3", "", { "dependencies": { "@semantic-release/error": "^3.0.0", "aggregate-error": "^3.0.0", "fs-extra": "^11.0.0", "lodash": "^4.17.4" }, "peerDependencies": { "semantic-release": ">=18.0.0" } }, "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag=="], "@semantic-release/commit-analyzer": ["@semantic-release/commit-analyzer@13.0.1", "", { "dependencies": { "conventional-changelog-angular": "^8.0.0", "conventional-changelog-writer": "^8.0.0", "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.0.0", "debug": "^4.0.0", "import-from-esm": "^2.0.0", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, "peerDependencies": { "semantic-release": ">=20.1.0" } }, "sha512-wdnBPHKkr9HhNhXOhZD5a2LNl91+hs8CC2vsAVYxtZH3y0dV3wKn+uZSN61rdJQZ8EGxzWB3inWocBHV9+u/CQ=="], @@ -899,6 +927,16 @@ "@swc/types": ["@swc/types@0.1.26", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw=="], + "@textlint/ast-node-types": ["@textlint/ast-node-types@15.8.0", "", {}, "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw=="], + + "@textlint/linter-formatter": ["@textlint/linter-formatter@15.8.0", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.8.0", "@textlint/resolver": "15.8.0", "@textlint/types": "15.8.0", "debug": "^4.4.3", "js-yaml": "^4.3.0", "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w=="], + + "@textlint/module-interop": ["@textlint/module-interop@15.8.0", "", {}, "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg=="], + + "@textlint/resolver": ["@textlint/resolver@15.8.0", "", {}, "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA=="], + + "@textlint/types": ["@textlint/types@15.8.0", "", { "dependencies": { "@textlint/ast-node-types": "15.8.0" } }, "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -997,6 +1035,8 @@ "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], @@ -1037,6 +1077,8 @@ "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], + "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], @@ -1501,6 +1543,8 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-from-esm": ["import-from-esm@2.0.0", "", { "dependencies": { "debug": "^4.3.4", "import-meta-resolve": "^4.0.0" } }, "sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g=="], @@ -1685,6 +1729,8 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], + "lodash.uniqby": ["lodash.uniqby@4.7.0", "", {}, "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww=="], "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], @@ -1911,7 +1957,7 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], @@ -1979,6 +2025,8 @@ "playwright-extra": ["playwright-extra@4.3.6", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "playwright": "*", "playwright-core": "*" }, "optionalPeers": ["playwright", "playwright-core"] }, "sha512-q2rVtcE8V8K3vPVF1zny4pvwZveHLH8KBuVU2MoE3Jw4OKVoBWsHI9CH9zPydovHHOCDxjGN2Vg+2m644q3ijA=="], + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], @@ -2059,6 +2107,8 @@ "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "rc-config-loader": ["rc-config-loader@4.1.4", "", { "dependencies": { "debug": "^4.4.3", "js-yaml": "^4.1.1", "json5": "^2.2.3", "require-from-string": "^2.0.2" } }, "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ=="], + "read-excel-file": ["read-excel-file@9.0.10", "", { "dependencies": { "@xmldom/xmldom": "^0.9.9", "fflate": "^0.8.2", "unzipper": "^0.12.3" } }, "sha512-Y0KhCerC+ybj2IrS5htrGRc7LXtyQT1rJ6MGqF28kJeRYjRt0HN1ne0jd0Xnwqh2gTJRs3fe0D9TLUYrBhT+LQ=="], "read-package-up": ["read-package-up@12.0.0", "", { "dependencies": { "find-up-simple": "^1.0.1", "read-pkg": "^10.0.0", "type-fest": "^5.2.0" } }, "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw=="], @@ -2117,6 +2167,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "secretlint": ["secretlint@13.0.2", "", { "dependencies": { "@secretlint/config-creator": "13.0.2", "@secretlint/formatter": "13.0.2", "@secretlint/node": "13.0.2", "@secretlint/profiler": "13.0.2", "@secretlint/resolver": "13.0.2", "@secretlint/walker": "13.0.2", "debug": "^4.4.3", "read-pkg": "^10.1.0" }, "bin": { "secretlint": "bin/secretlint.js" } }, "sha512-veYNpVC+Yw9H/EWzdGyEsYHqCFXgKOinGEMbRffvnkHoWnE0CongrCnWXZJ1bkYmirlhemq/gkiOZ5zdHcHCQg=="], + "seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="], "semantic-release": ["semantic-release@25.0.3", "", { "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", "@semantic-release/github": "^12.0.0", "@semantic-release/npm": "^13.1.1", "@semantic-release/release-notes-generator": "^14.1.0", "aggregate-error": "^5.0.0", "cosmiconfig": "^9.0.0", "debug": "^4.0.0", "env-ci": "^11.0.0", "execa": "^9.0.0", "figures": "^6.0.0", "find-versions": "^6.0.0", "get-stream": "^6.0.0", "git-log-parser": "^1.2.0", "hook-std": "^4.0.0", "hosted-git-info": "^9.0.0", "import-from-esm": "^2.0.0", "lodash-es": "^4.17.21", "marked": "^15.0.0", "marked-terminal": "^7.3.0", "micromatch": "^4.0.2", "p-each-series": "^3.0.0", "p-reduce": "^3.0.0", "read-package-up": "^12.0.0", "resolve-from": "^5.0.0", "semver": "^7.3.2", "signale": "^1.2.1", "yargs": "^18.0.0" }, "bin": { "semantic-release": "bin/semantic-release.js" } }, "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA=="], @@ -2235,6 +2287,8 @@ "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + "structured-source": ["structured-source@4.0.0", "", { "dependencies": { "boundary": "^2.0.0" } }, "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA=="], + "super-regex": ["super-regex@1.1.0", "", { "dependencies": { "function-timeout": "^1.0.1", "make-asynchronous": "^1.0.1", "time-span": "^5.1.0" } }, "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -2245,6 +2299,8 @@ "sylvester": ["sylvester@0.0.21", "", {}, "sha512-yUT0ukFkFEt4nb+NY+n2ag51aS/u9UHXoZw+A4jgD77/jzZsBoSDHuqysrVCBC4CYR4TYvUJq54ONpXgDBH8tA=="], + "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], @@ -2253,10 +2309,14 @@ "tempy": ["tempy@3.2.0", "", { "dependencies": { "is-stream": "^3.0.0", "temp-dir": "^3.0.0", "type-fest": "^2.12.2", "unique-string": "^3.0.0" } }, "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ=="], + "terminal-link": ["terminal-link@5.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^4.1.0" } }, "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA=="], + "text-extensions": ["text-extensions@3.1.0", "", {}, "sha512-anOjtXr8OT5w4vc/2mP4AYTCE0GWc/21icGmaHtBHnI7pN7o01a/oqG9m06/rGzoAsDm/WNzggBpqptuCmRlZQ=="], "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -2459,6 +2519,8 @@ "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], + "@secretlint/config-loader/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "@semantic-release/github/@semantic-release/error": ["@semantic-release/error@4.0.0", "", {}, "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ=="], "@semantic-release/github/aggregate-error": ["aggregate-error@5.0.0", "", { "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" } }, "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw=="], @@ -2477,6 +2539,16 @@ "@stryker-mutator/core/execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "@textlint/linter-formatter/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], + + "@textlint/linter-formatter/lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "@textlint/linter-formatter/pluralize": ["pluralize@2.0.0", "", {}, "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw=="], + + "@textlint/linter-formatter/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@types/adm-zip/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@typespec/ts-http-runtime/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -2521,6 +2593,8 @@ "constantinople/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "cosmiconfig-typescript-loader/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -2905,6 +2979,8 @@ "parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "parse-json/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], "path-scurry/lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], @@ -2923,8 +2999,6 @@ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "read-pkg/parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], - "read-pkg/unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -2965,12 +3039,22 @@ "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "table/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "table/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "table/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], "tempy/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], "tempy/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "terminal-link/supports-hyperlinks": ["supports-hyperlinks@4.5.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w=="], + "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], @@ -3029,6 +3113,10 @@ "@stryker-mutator/core/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "@textlint/linter-formatter/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@types/adm-zip/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@typespec/ts-http-runtime/http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -3073,6 +3161,8 @@ "constantinople/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "cosmiconfig/parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "duplexer2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "engine.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -3129,10 +3219,6 @@ "promptfoo/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], - "read-pkg/parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "read-pkg/parse-json/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "semantic-release/@semantic-release/github/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -3169,6 +3255,16 @@ "stream-combiner2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "table/slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "table/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "terminal-link/supports-hyperlinks/has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], + + "terminal-link/supports-hyperlinks/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -3183,8 +3279,6 @@ "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], - "@semantic-release/release-notes-generator/read-package-up/read-pkg/parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], - "@semantic-release/release-notes-generator/read-package-up/read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], "blamer/execa/npm-run-path/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -3203,14 +3297,14 @@ "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "cosmiconfig/parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "env-ci/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], "npm/minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "npm/minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "read-pkg/parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "semantic-release/@semantic-release/github/http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -3221,12 +3315,12 @@ "signale/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "table/slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - "@semantic-release/release-notes-generator/read-package-up/read-pkg/parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "cli-highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -3237,9 +3331,9 @@ "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "table/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "@semantic-release/release-notes-generator/read-package-up/read-pkg/parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], diff --git a/knip.json b/knip.json index 63a3209..f73cb47 100644 --- a/knip.json +++ b/knip.json @@ -27,6 +27,7 @@ "@semantic-release/github", "promptfoo", "@stryker-mutator/bun-runner", - "@stryker-mutator/api" + "@stryker-mutator/api", + "@secretlint/secretlint-rule-preset-recommend" ] } diff --git a/package.json b/package.json index d18e8c9..b8a5657 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "knip": "knip-bun", "klint": "klint", "jscpd": "jscpd --noTips", + "secretlint": "secretlint \"**/*\"", "lint-staged": "lint-staged", "prepare": "bun .husky/install.mjs", "build:skill-tools": "bun run scripts/build-skill-tools.ts", @@ -71,6 +72,7 @@ "@hughescr/stryker-bun-runner": "1.3.8", "@konvert7/klint": "0.20.0", "@opencode-ai/plugin": "latest", + "@secretlint/secretlint-rule-preset-recommend": "13.0.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.8", @@ -83,6 +85,7 @@ "knip": "^6.14.1", "lint-staged": "17.0.5", "promptfoo": "0.121.14", + "secretlint": "13.0.2", "semantic-release": "^25.0.3", "typescript": "^5.9.3" }, From 822508f7fbe3500a92b04c78a4b3390cf205e4fc Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 11:22:25 +0200 Subject: [PATCH 10/35] chore: add madge to the gate chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - detect circular imports across src - run it at session end in every agent, on pre-commit, and in CI Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .agents/hooks/madge.ts | 4 + .claude/settings.json | 4 + .codex/hooks.json | 8 + .cursor/hooks.json | 3 + .github/workflows/ci.yml | 3 + .husky/pre-commit | 2 +- .opencode/plugins/lint.ts | 1 + bun.lock | 309 ++++++++++++++++++++++++++++++-------- package.json | 2 + 9 files changed, 272 insertions(+), 64 deletions(-) create mode 100644 .agents/hooks/madge.ts diff --git a/.agents/hooks/madge.ts b/.agents/hooks/madge.ts new file mode 100644 index 0000000..cb713d1 --- /dev/null +++ b/.agents/hooks/madge.ts @@ -0,0 +1,4 @@ +import { runHook } from "./run-hook"; + +const exitCode = runHook(["bun", "run", "madge"]); +process.exit(exitCode); diff --git a/.claude/settings.json b/.claude/settings.json index 93d304f..6076bbe 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -27,6 +27,10 @@ { "type": "command", "command": "bun .agents/hooks/secretlint.ts" + }, + { + "type": "command", + "command": "bun .agents/hooks/madge.ts" } ] } diff --git a/.codex/hooks.json b/.codex/hooks.json index 9e24538..5a85a2a 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -49,6 +49,14 @@ "command": "bun .agents/hooks/secretlint.ts --codex" } ] + }, + { + "hooks": [ + { + "type": "command", + "command": "bun .agents/hooks/madge.ts --codex" + } + ] } ] } diff --git a/.cursor/hooks.json b/.cursor/hooks.json index 7c633a1..7181591 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -19,6 +19,9 @@ }, { "command": "bun .agents/hooks/secretlint.ts" + }, + { + "command": "bun .agents/hooks/madge.ts" } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 228b7d2..94009c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: - name: Klint run: bun run klint + - name: Madge + run: bun run madge + - name: Secretlint run: bun run secretlint diff --git a/.husky/pre-commit b/.husky/pre-commit index fcba857..51d3e9c 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -bun run lint-staged && bun run type-check && bun run knip && bun run jscpd && bun run klint && bun run secretlint +bun run lint-staged && bun run type-check && bun run knip && bun run jscpd && bun run klint && bun run madge && bun run secretlint diff --git a/.opencode/plugins/lint.ts b/.opencode/plugins/lint.ts index dd15f81..609a302 100644 --- a/.opencode/plugins/lint.ts +++ b/.opencode/plugins/lint.ts @@ -11,6 +11,7 @@ export const LintPlugin: Plugin = async ({ $ }) => { await $`bun run knip`; await $`bun run jscpd`; await $`bun klint/cli.ts --json`; + await $`bun run madge`; await $`bun run secretlint`; }, }; diff --git a/bun.lock b/bun.lock index aab5593..3cd4b47 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "jscpd": "^4.2.3", "knip": "^6.14.1", "lint-staged": "17.0.5", + "madge": "8.0.0", "promptfoo": "0.121.14", "secretlint": "13.0.2", "semantic-release": "^25.0.3", @@ -329,6 +330,10 @@ "@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="], + "@dependents/detective-less": ["@dependents/detective-less@5.0.3", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA=="], + + "@discoveryjs/json-ext": ["@discoveryjs/json-ext@1.1.0", "", {}, "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -941,6 +946,14 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@ts-graphviz/adapter": ["@ts-graphviz/adapter@2.0.6", "", { "dependencies": { "@ts-graphviz/common": "^2.1.5" } }, "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q=="], + + "@ts-graphviz/ast": ["@ts-graphviz/ast@2.0.7", "", { "dependencies": { "@ts-graphviz/common": "^2.1.5" } }, "sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw=="], + + "@ts-graphviz/common": ["@ts-graphviz/common@2.1.5", "", {}, "sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg=="], + + "@ts-graphviz/core": ["@ts-graphviz/core@2.0.7", "", { "dependencies": { "@ts-graphviz/ast": "^2.0.7", "@ts-graphviz/common": "^2.1.5" } }, "sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/adm-zip": ["@types/adm-zip@0.5.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q=="], @@ -975,10 +988,30 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.67.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.67.0", "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.67.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.67.0", "", {}, "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.67.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.67.0", "@typescript-eslint/tsconfig-utils": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.5", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw=="], "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + "@vue/compiler-core": ["@vue/compiler-core@3.5.41", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/shared": "3.5.41", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.41", "", { "dependencies": { "@vue/compiler-core": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.41", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/compiler-core": "3.5.41", "@vue/compiler-dom": "3.5.41", "@vue/compiler-ssr": "3.5.41", "@vue/shared": "3.5.41", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.41", "", { "dependencies": { "@vue/compiler-dom": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A=="], + + "@vue/shared": ["@vue/shared@3.5.41", "", {}, "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="], "a-sync-waterfall": ["a-sync-waterfall@1.0.1", "", {}, "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA=="], @@ -1011,10 +1044,12 @@ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "app-module-path": ["app-module-path@2.2.0", "", {}, "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ=="], + "apparatus": ["apparatus@0.0.10", "", { "dependencies": { "sylvester": ">= 0.0.8" } }, "sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg=="], "aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="], @@ -1033,6 +1068,8 @@ "assert-never": ["assert-never@1.4.0", "", {}, "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA=="], + "ast-module-types": ["ast-module-types@6.0.2", "", {}, "sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw=="], + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], @@ -1067,6 +1104,8 @@ "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "blamer": ["blamer@1.0.7", "", { "dependencies": { "execa": "^4.0.0", "which": "^2.0.2" } }, "sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA=="], "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], @@ -1089,6 +1128,8 @@ "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], @@ -1127,13 +1168,13 @@ "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], - "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], @@ -1143,13 +1184,15 @@ "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="], @@ -1161,6 +1204,8 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], "complex.js": ["complex.js@2.4.3", "", {}, "sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ=="], @@ -1237,6 +1282,8 @@ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], @@ -1251,12 +1298,32 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dependency-tree": ["dependency-tree@11.5.0", "", { "dependencies": { "@discoveryjs/json-ext": "^1.1.0", "commander": "^12.1.0", "filing-cabinet": "^5.5.1", "precinct": "^12.3.2", "typescript": "^5.9.3" }, "bin": { "dependency-tree": "bin/cli.js" } }, "sha512-K9zBwKDZrot3RkxizugpVSdImxULAg4Ycp3+ydy2r561k96oiiw6nfsOR15fwNDQ5BF2UXe+2JFM/H5Xz4MGQg=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "detective-amd": ["detective-amd@6.1.0", "", { "dependencies": { "ast-module-types": "^6.0.1", "escodegen": "^2.1.0", "get-amd-module-type": "^6.0.2", "node-source-walk": "^7.0.1" }, "bin": { "detective-amd": "bin/cli.js" } }, "sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg=="], + + "detective-cjs": ["detective-cjs@6.1.1", "", { "dependencies": { "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" } }, "sha512-pSh7mkCKEtLlmANqLu3KDFS3NV8Hx41jy/JF1/gAWOgU+Uo5QTkeI1tWNP4dWGo4L0E9j18Ez9EPsTleautKqA=="], + + "detective-es6": ["detective-es6@5.0.2", "", { "dependencies": { "node-source-walk": "^7.0.1" } }, "sha512-+qHHGYhjupiVs4rnIpI9nZ5B130A4AmE35ZX1w33hb46vcZ7T3jfDbvmPw0FhWtMHn5BS5HHu7ZtnZ53bMcXZA=="], + + "detective-postcss": ["detective-postcss@8.0.4", "", { "dependencies": { "is-url-superb": "^4.0.0", "postcss-values-parser": "^6.0.2" }, "peerDependencies": { "postcss": "^8.4.47" } }, "sha512-DZ7M/hWPZyr17ZUdoQ+TVXaPj70mYr4XXrAE+GeJbca44haCvZgb191L/jLJmFYewhxRJuBd4lUtNSu986TXag=="], + + "detective-sass": ["detective-sass@6.0.2", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-i3xpXHDKS0qI2aFW4asQ7fqlPK00ndOVZELvQapFJCaF0VxYmsNWtd0AmvXbTLMk7bfO5VdIeorhY9KfmHVoVA=="], + + "detective-scss": ["detective-scss@5.0.2", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-9JOEMZ8pDh3ShXmftq7hoQqqJsClaGgxo1hghfCeFlmKf5TC/Twtwb0PAaK8dXwpg9Z0uCmEYSrCxO+kel2eEg=="], + + "detective-stylus": ["detective-stylus@5.0.1", "", {}, "sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA=="], + + "detective-typescript": ["detective-typescript@14.1.2", "", { "dependencies": { "@typescript-eslint/typescript-estree": "^8.58.2", "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" }, "peerDependencies": { "typescript": "^5.4.4 || ^6.0.2" } }, "sha512-bIeEn0eVi/JRsE1YizBR2ilnMlWRAIBJJ6kXCKNFxEEWhUcEY3R6I3KYIAy48ieURbD1hcb3Ebvl8AqeoPMSzg=="], + + "detective-vue2": ["detective-vue2@2.3.0", "", { "dependencies": { "@dependents/detective-less": "^5.0.1", "@vue/compiler-sfc": "^3.5.32", "detective-es6": "^5.0.1", "detective-sass": "^6.0.1", "detective-scss": "^5.0.1", "detective-stylus": "^5.0.1", "detective-typescript": "^14.1.0" }, "peerDependencies": { "typescript": "^5.4.4 || ^6.0.2" } }, "sha512-3gwbZPqVTm9sL9XdZsgEJ7x4x99O853VVZHapQAiEkGuMJMpFPjHDrecSgfqnS5JW3FJfYXesLZGvUOibjn49g=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], @@ -1299,6 +1366,8 @@ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], + "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], "env-ci": ["env-ci@11.2.0", "", { "dependencies": { "execa": "^8.0.0", "java-properties": "^1.0.2" } }, "sha512-D5kWfzkmaOQDioPmiviWAVtKmpPT4/iJmMVQxWxMPJTFyTkdc5JQUfc5iXEeWxcOdsYTKSAiA/Age4NUOqKsRA=="], @@ -1335,10 +1404,14 @@ "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -1405,6 +1478,8 @@ "file-type": ["file-type@21.3.2", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w=="], + "filing-cabinet": ["filing-cabinet@5.5.1", "", { "dependencies": { "app-module-path": "^2.2.0", "commander": "^12.1.0", "enhanced-resolve": "^5.21.0", "module-definition": "^6.0.2", "module-lookup-amd": "^9.1.3", "resolve": "^1.22.12", "resolve-dependency-path": "^4.0.1", "sass-lookup": "^6.1.2", "stylus-lookup": "^6.1.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.9.3" }, "bin": { "filing-cabinet": "bin/cli.js" } }, "sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -1457,12 +1532,16 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-amd-module-type": ["get-amd-module-type@6.0.2", "", { "dependencies": { "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" } }, "sha512-7zShVYAYtMnj9S65CfN+hvpBCByfuB1OY8xID01nZEzXTZbx4YyysAfi+nMl95JSR6odt4q8TCj2W63KAoyVLQ=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -1485,6 +1564,8 @@ "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + "gonzales-pe": ["gonzales-pe@4.3.0", "", { "dependencies": { "minimist": "^1.2.5" }, "bin": { "gonzales": "bin/gonzales.js" } }, "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ=="], + "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -1587,7 +1668,7 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], @@ -1599,9 +1680,13 @@ "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + "is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "is-url-superb": ["is-url-superb@4.0.0", "", {}, "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA=="], "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], @@ -1733,7 +1818,7 @@ "lodash.uniqby": ["lodash.uniqby@4.7.0", "", {}, "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww=="], - "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], @@ -1743,6 +1828,10 @@ "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], + "madge": ["madge@8.0.0", "", { "dependencies": { "chalk": "^4.1.2", "commander": "^7.2.0", "commondir": "^1.0.1", "debug": "^4.3.4", "dependency-tree": "^11.0.0", "ora": "^5.4.1", "pluralize": "^8.0.0", "pretty-ms": "^7.0.1", "rc": "^1.2.8", "stream-to-array": "^2.3.0", "ts-graphviz": "^2.1.2", "walkdir": "^0.4.1" }, "peerDependencies": { "typescript": "^5.4.4" }, "optionalPeers": ["typescript"], "bin": { "madge": "bin/cli.js" } }, "sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "make-asynchronous": ["make-asynchronous@1.1.0", "", { "dependencies": { "p-event": "^6.0.0", "type-fest": "^4.6.0", "web-worker": "^1.5.0" } }, "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg=="], "make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], @@ -1801,6 +1890,10 @@ "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "module-definition": ["module-definition@6.0.2", "", { "dependencies": { "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" }, "bin": { "module-definition": "bin/cli.js" } }, "sha512-SvAU3lB0+Yjbq55yHY3wkRZBOh+fhU1SnIF3IFbTewv6mtAh7yUT8ACHAJ2mGIJ7tCes2QuCL/cl6m0JSZ/ArA=="], + + "module-lookup-amd": ["module-lookup-amd@9.1.3", "", { "dependencies": { "commander": "^12.1.0", "requirejs": "^2.3.8", "requirejs-config-file": "^4.0.0" }, "bin": { "lookup-amd": "bin/cli.js" } }, "sha512-Jc3XmOaR9FdfMJSK8+vyLgsCkzm8z2L0NS6vrlRWi12DjS7MY7TMNE7E1yj8yXx837xtMDbKSSgcdXnFlJ2YLg=="], + "mongodb": ["mongodb@7.2.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.2.0", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw=="], "mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="], @@ -1835,6 +1928,8 @@ "nan": ["nan@2.27.0", "", {}, "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ=="], + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + "natural": ["natural@8.1.1", "", { "dependencies": { "afinn-165": "^2.0.2", "afinn-165-financialmarketnews": "^3.0.0", "apparatus": "^0.0.10", "dotenv": "^17.3.1", "memjs": "^1.3.2", "mongoose": "^9.2.1", "pg": "^8.18.0", "redis": "^5.11.0", "safe-stable-stringify": "^2.5.0", "stopwords-iso": "^1.1.0", "sylvester": "^0.0.21", "underscore": "^1.13.0", "uuid": "^13.0.0", "wordnet-db": "^3.1.14" } }, "sha512-Ucb+lsUcGxUqu3rn8cwHjT6gJQosO63nIX/aBQXB3+IDkNbFV7PuviysO+Rzz3aKn7PZhPj3bNF4PS9gDVjYCQ=="], "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], @@ -1863,6 +1958,8 @@ "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], + "node-source-walk": ["node-source-walk@7.0.2", "", { "dependencies": { "@babel/parser": "^7.29.0" } }, "sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A=="], + "node-sql-parser": ["node-sql-parser@5.4.0", "", { "dependencies": { "@types/pegjs": "^0.10.0", "big-integer": "^1.6.48" } }, "sha512-jVe6Z61gPcPjCElPZ6j8llB3wnqGcuQzefim1ERsqIakxnEy5JlzV7XKdO1KmacRG5TKwPc4vJTgSRQ0LfkbFw=="], "nopt": ["nopt@5.0.0", "", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="], @@ -1911,7 +2008,7 @@ "opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="], - "ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="], + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], @@ -1959,7 +2056,7 @@ "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parse-ms": ["parse-ms@2.1.0", "", {}, "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="], "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], @@ -2027,6 +2124,10 @@ "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "postcss-values-parser": ["postcss-values-parser@6.0.2", "", { "dependencies": { "color-name": "^1.1.4", "is-url-superb": "^4.0.0", "quote-unquote": "^1.0.0" }, "peerDependencies": { "postcss": "^8.2.9" } }, "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], @@ -2037,7 +2138,9 @@ "posthog-node": ["posthog-node@5.24.17", "", { "dependencies": { "@posthog/core": "1.23.1" } }, "sha512-mdb8TKt+YCRbGQdYar3AKNUPCyEiqcprScF4unYpGALF6HlBaEuO6wPuIqXXpCWkw4VclJYCKbb6lq6pH6bJeA=="], - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "precinct": ["precinct@12.3.2", "", { "dependencies": { "@dependents/detective-less": "^5.0.3", "commander": "^12.1.0", "detective-amd": "^6.1.0", "detective-cjs": "^6.1.1", "detective-es6": "^5.0.2", "detective-postcss": "^8.0.3", "detective-sass": "^6.0.2", "detective-scss": "^5.0.2", "detective-stylus": "^5.0.1", "detective-typescript": "^14.1.2", "detective-vue2": "^2.3.0", "module-definition": "^6.0.2", "node-source-walk": "^7.0.2", "postcss": "^8.5.14", "typescript": "^5.9.3" }, "bin": { "precinct": "bin/cli.js" } }, "sha512-JbJevI1K80z8e/WIyDt/4vUN/4qcfBSKKqOjJA4mosPPPb7zODKRJQV7YN7apVWN3k58nZYm/vEsLgEGYmnxwg=="], + + "pretty-ms": ["pretty-ms@7.0.1", "", { "dependencies": { "parse-ms": "^2.1.0" } }, "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], @@ -2101,6 +2204,8 @@ "quickjs-wasi": ["quickjs-wasi@2.2.0", "", {}, "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw=="], + "quote-unquote": ["quote-unquote@1.0.0", "", {}, "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], @@ -2129,15 +2234,21 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "requirejs": ["requirejs@2.3.8", "", { "bin": { "r.js": "bin/r.js", "r_js": "bin/r.js" } }, "sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw=="], + + "requirejs-config-file": ["requirejs-config-file@4.0.0", "", { "dependencies": { "esprima": "^4.0.0", "stringify-object": "^3.2.1" } }, "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw=="], + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + "resolve-dependency-path": ["resolve-dependency-path@4.0.1", "", {}, "sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ=="], + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], @@ -2167,6 +2278,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sass-lookup": ["sass-lookup@6.1.2", "", { "dependencies": { "commander": "^12.1.0", "enhanced-resolve": "^5.20.0" }, "bin": { "sass-lookup": "bin/cli.js" } }, "sha512-GjmndmKQBtlPil79RK72L7yc5kDXZPCQeH97bP8R8DcxtXQJO6vECExb3WP/m6+cxaV9h4ZxrSRvCkPG2v/VSw=="], + "secretlint": ["secretlint@13.0.2", "", { "dependencies": { "@secretlint/config-creator": "13.0.2", "@secretlint/formatter": "13.0.2", "@secretlint/node": "13.0.2", "@secretlint/profiler": "13.0.2", "@secretlint/resolver": "13.0.2", "@secretlint/walker": "13.0.2", "debug": "^4.4.3", "read-pkg": "^10.1.0" }, "bin": { "secretlint": "bin/secretlint.js" } }, "sha512-veYNpVC+Yw9H/EWzdGyEsYHqCFXgKOinGEMbRffvnkHoWnE0CongrCnWXZJ1bkYmirlhemq/gkiOZ5zdHcHCQg=="], "seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="], @@ -2239,6 +2352,8 @@ "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "spark-md5": ["spark-md5@3.0.2", "", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="], "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], @@ -2269,13 +2384,17 @@ "stream-combiner2": ["stream-combiner2@1.1.1", "", { "dependencies": { "duplexer2": "~0.1.0", "readable-stream": "^2.0.2" } }, "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw=="], + "stream-to-array": ["stream-to-array@2.3.0", "", { "dependencies": { "any-promise": "^1.1.0" } }, "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], @@ -2289,6 +2408,8 @@ "structured-source": ["structured-source@4.0.0", "", { "dependencies": { "boundary": "^2.0.0" } }, "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA=="], + "stylus-lookup": ["stylus-lookup@6.1.2", "", { "dependencies": { "commander": "^12.1.0" }, "bin": { "stylus-lookup": "bin/cli.js" } }, "sha512-O+Q/SJ8s1X2aMLh4213fQ9X/bND9M3dhSsyTRe+O1OXPcewGLiYmAtKCrnP7FDvDBaXB2ZHPkCt3zi4cJXBlCQ=="], + "super-regex": ["super-regex@1.1.0", "", { "dependencies": { "function-timeout": "^1.0.1", "make-asynchronous": "^1.0.1", "time-span": "^5.1.0" } }, "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -2303,6 +2424,8 @@ "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], @@ -2355,6 +2478,12 @@ "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "ts-graphviz": ["ts-graphviz@2.1.6", "", { "dependencies": { "@ts-graphviz/adapter": "^2.0.6", "@ts-graphviz/ast": "^2.0.7", "@ts-graphviz/common": "^2.1.5", "@ts-graphviz/core": "^2.0.7" } }, "sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw=="], + + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], @@ -2423,6 +2552,10 @@ "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + "walkdir": ["walkdir@0.4.1", "", {}, "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], @@ -2521,6 +2654,8 @@ "@secretlint/config-loader/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "@secretlint/formatter/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "@semantic-release/github/@semantic-release/error": ["@semantic-release/error@4.0.0", "", {}, "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ=="], "@semantic-release/github/aggregate-error": ["aggregate-error@5.0.0", "", { "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" } }, "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw=="], @@ -2547,14 +2682,18 @@ "@textlint/linter-formatter/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@types/adm-zip/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@typespec/ts-http-runtime/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "@vue/compiler-core/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -2579,6 +2718,8 @@ "cli-truncate/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "color/color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], @@ -2603,6 +2744,8 @@ "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], + "dependency-tree/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "engine.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -2623,14 +2766,16 @@ "express/qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + "figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "filing-cabinet/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "from2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "gauge/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "gauge/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@8.0.0", "", {}, "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ=="], @@ -2667,12 +2812,22 @@ "load-json-file/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + "log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "logform/@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + "madge/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "madge/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "make-asynchronous/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -2683,8 +2838,12 @@ "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "module-lookup-amd/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "mongodb-connection-string-url/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "node-source-walk/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + "npm/@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "npm/@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -2971,7 +3130,7 @@ "onnxruntime-web/protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], - "ora/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "p-event/p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], @@ -2989,8 +3148,12 @@ "pgpass/split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "precinct/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "promptfoo/execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "promptfoo/ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="], + "promptfoo/undici": ["undici@7.27.1", "", {}, "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg=="], "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], @@ -3001,12 +3164,10 @@ "read-pkg/unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="], - "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - - "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "sass-lookup/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "semantic-release/@semantic-release/error": ["@semantic-release/error@4.0.0", "", {}, "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ=="], "semantic-release/@semantic-release/github": ["@semantic-release/github@12.0.6", "", { "dependencies": { "@octokit/core": "^7.0.0", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-retry": "^8.0.0", "@octokit/plugin-throttling": "^11.0.0", "@semantic-release/error": "^4.0.0", "aggregate-error": "^5.0.0", "debug": "^4.3.4", "dir-glob": "^3.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "issue-parser": "^7.0.0", "lodash-es": "^4.17.21", "mime": "^4.0.0", "p-filter": "^4.0.0", "tinyglobby": "^0.2.14", "undici": "^7.0.0", "url-join": "^5.0.0" }, "peerDependencies": { "semantic-release": ">=24.1.0" } }, "sha512-aYYFkwHW3c6YtHwQF0t0+lAjlU+87NFOZuH2CvWFD0Ylivc7MwhZMiHOJ0FMpIgPpCVib/VUAcOwvrW0KnxQtA=="], @@ -3027,6 +3188,8 @@ "signale/figures": ["figures@2.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "socket.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -3037,16 +3200,22 @@ "stream-combiner2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "stringify-object/is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="], + + "strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "stylus-lookup/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "table/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "table/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], "table/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], "tempy/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], @@ -3071,8 +3240,12 @@ "with/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "wrap-ansi/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -3095,6 +3268,8 @@ "@semantic-release/npm/execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + "@semantic-release/npm/execa/pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "@semantic-release/npm/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "@semantic-release/npm/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], @@ -3109,14 +3284,14 @@ "@stryker-mutator/core/execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + "@stryker-mutator/core/execa/pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "@stryker-mutator/core/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "@stryker-mutator/core/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], "@textlint/linter-formatter/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@types/adm-zip/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@typespec/ts-http-runtime/http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -3137,8 +3312,6 @@ "blamer/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "cli-highlight/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -3147,11 +3320,11 @@ "cli-progress/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cli-progress/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cli-table3/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "color/color-convert/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], @@ -3189,16 +3362,20 @@ "gauge/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "gauge/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "ibm-cloud-sdk-core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "ibm-cloud-sdk-core/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "mongodb-connection-string-url/whatwg-url/tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], "mongodb-connection-string-url/whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], @@ -3215,10 +3392,24 @@ "promptfoo/execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + "promptfoo/execa/pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "promptfoo/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "promptfoo/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "promptfoo/ora/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "promptfoo/ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + + "promptfoo/ora/is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "promptfoo/ora/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "promptfoo/ora/log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "promptfoo/ora/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "semantic-release/@semantic-release/github/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -3237,6 +3428,8 @@ "semantic-release/execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + "semantic-release/execa/pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "semantic-release/execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "semantic-release/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], @@ -3255,12 +3448,8 @@ "stream-combiner2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "table/slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "table/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "terminal-link/supports-hyperlinks/has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], "terminal-link/supports-hyperlinks/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], @@ -3271,40 +3460,42 @@ "wide-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "wide-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "with/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "with/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@semantic-release/npm/execa/pretty-ms/parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], "@semantic-release/release-notes-generator/read-package-up/read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], - "blamer/execa/npm-run-path/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "@stryker-mutator/core/execa/pretty-ms/parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - "cli-highlight/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "blamer/execa/npm-run-path/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "cli-highlight/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cli-progress/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "cosmiconfig/parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "env-ci/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "npm/minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "npm/minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "promptfoo/execa/pretty-ms/parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "promptfoo/ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "promptfoo/ora/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "semantic-release/@semantic-release/github/http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -3313,30 +3504,22 @@ "semantic-release/@semantic-release/github/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "signale/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "semantic-release/execa/pretty-ms/parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - "table/slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "signale/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "signale/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - "cli-highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "promptfoo/ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "cli-highlight/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "promptfoo/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "table/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "signale/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "@semantic-release/release-notes-generator/read-package-up/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/package.json b/package.json index b8a5657..3f18ca7 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "knip": "knip-bun", "klint": "klint", "jscpd": "jscpd --noTips", + "madge": "madge --circular --extensions ts --ts-config tsconfig.json src", "secretlint": "secretlint \"**/*\"", "lint-staged": "lint-staged", "prepare": "bun .husky/install.mjs", @@ -84,6 +85,7 @@ "jscpd": "^4.2.3", "knip": "^6.14.1", "lint-staged": "17.0.5", + "madge": "8.0.0", "promptfoo": "0.121.14", "secretlint": "13.0.2", "semantic-release": "^25.0.3", From 69576c384b0efb84f2b13337ad2339546473eb4b Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 11:44:30 +0200 Subject: [PATCH 11/35] chore: add a line-ending gate with a fix mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fail when a tracked file carries CRLF in the index or the worktree - convert offenders to LF with lf:fix, leaving binary content untouched - run the check at session end in every agent, on pre-commit, and in CI Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .agents/hooks/lf.ts | 4 +++ .agents/scripts/check-lf.ts | 62 +++++++++++++++++++++++++++++++++++++ .claude/settings.json | 4 +++ .codex/hooks.json | 8 +++++ .cursor/hooks.json | 3 ++ .github/workflows/ci.yml | 3 ++ .husky/pre-commit | 2 +- .opencode/plugins/lint.ts | 1 + package.json | 2 ++ test/check-lf.test.ts | 45 +++++++++++++++++++++++++++ 10 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 .agents/hooks/lf.ts create mode 100644 .agents/scripts/check-lf.ts create mode 100644 test/check-lf.test.ts diff --git a/.agents/hooks/lf.ts b/.agents/hooks/lf.ts new file mode 100644 index 0000000..36b09f1 --- /dev/null +++ b/.agents/hooks/lf.ts @@ -0,0 +1,4 @@ +import { runHook } from "./run-hook"; + +const exitCode = runHook(["bun", "run", "lf"]); +process.exit(exitCode); diff --git a/.agents/scripts/check-lf.ts b/.agents/scripts/check-lf.ts new file mode 100644 index 0000000..b0e8263 --- /dev/null +++ b/.agents/scripts/check-lf.ts @@ -0,0 +1,62 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; + +function listTrackedFilesWithEol(): string[] { + const r = spawnSync("git", ["ls-files", "--eol"], { encoding: "utf8" }); + if (r.status !== 0) { + process.stderr.write(r.stderr || "git ls-files --eol failed\n"); + process.exit(1); + } + return r.stdout.split("\n").filter(Boolean); +} + +export function hasCarriageReturn(eolLine: string): boolean { + const [index, worktree] = eolLine.split(/\s+/); + return /crlf|mixed/.test(index) || /crlf|mixed/.test(worktree); +} + +function pathFrom(eolLine: string): string { + return eolLine.split("\t").slice(1).join("\t"); +} + +// A NUL byte is git's own heuristic for binary content. Rewriting CR bytes inside +// an image or archive would corrupt it, so those are reported and left alone. +export function isBinary(contents: Buffer): boolean { + return contents.includes(0); +} + +function convertToLf(path: string): boolean { + const contents = readFileSync(path); + if (isBinary(contents)) return false; + writeFileSync(path, contents.toString("utf8").replaceAll("\r\n", "\n"), "utf8"); + return true; +} + +function reportAndExit(offenders: string[]): never { + process.stderr.write( + `CRLF found in tracked files (LF required):\n${offenders.join("\n")}\n` + + `Run 'bun run lf:fix' to convert them.\n` + ); + process.exit(1); +} + +function fixAndExit(offenders: string[]): never { + const skipped = offenders.filter((path) => !convertToLf(path)); + const converted = offenders.length - skipped.length; + process.stdout.write(`Converted ${converted} file(s) to LF.\n`); + if (skipped.length === 0) process.exit(0); + process.stderr.write(`Skipped binary file(s):\n${skipped.join("\n")}\n`); + process.exit(1); +} + +if (import.meta.main) { + const offenders = listTrackedFilesWithEol().filter(hasCarriageReturn).map(pathFrom); + + if (offenders.length === 0) { + process.stdout.write("All tracked files use LF line endings.\n"); + process.exit(0); + } + + if (process.argv.includes("--fix")) fixAndExit(offenders); + reportAndExit(offenders); +} diff --git a/.claude/settings.json b/.claude/settings.json index 6076bbe..da763ec 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -31,6 +31,10 @@ { "type": "command", "command": "bun .agents/hooks/madge.ts" + }, + { + "type": "command", + "command": "bun .agents/hooks/lf.ts" } ] } diff --git a/.codex/hooks.json b/.codex/hooks.json index 5a85a2a..2ac97bc 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -57,6 +57,14 @@ "command": "bun .agents/hooks/madge.ts --codex" } ] + }, + { + "hooks": [ + { + "type": "command", + "command": "bun .agents/hooks/lf.ts --codex" + } + ] } ] } diff --git a/.cursor/hooks.json b/.cursor/hooks.json index 7181591..15524ce 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -22,6 +22,9 @@ }, { "command": "bun .agents/hooks/madge.ts" + }, + { + "command": "bun .agents/hooks/lf.ts" } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94009c3..60affd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,9 @@ jobs: - name: Madge run: bun run madge + - name: Line endings + run: bun run lf + - name: Secretlint run: bun run secretlint diff --git a/.husky/pre-commit b/.husky/pre-commit index 51d3e9c..6a8def3 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -bun run lint-staged && bun run type-check && bun run knip && bun run jscpd && bun run klint && bun run madge && bun run secretlint +bun run lint-staged && bun run type-check && bun run knip && bun run jscpd && bun run klint && bun run madge && bun run lf && bun run secretlint diff --git a/.opencode/plugins/lint.ts b/.opencode/plugins/lint.ts index 609a302..c5c46f9 100644 --- a/.opencode/plugins/lint.ts +++ b/.opencode/plugins/lint.ts @@ -12,6 +12,7 @@ export const LintPlugin: Plugin = async ({ $ }) => { await $`bun run jscpd`; await $`bun klint/cli.ts --json`; await $`bun run madge`; + await $`bun run lf`; await $`bun run secretlint`; }, }; diff --git a/package.json b/package.json index 3f18ca7..35b25ab 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "klint": "klint", "jscpd": "jscpd --noTips", "madge": "madge --circular --extensions ts --ts-config tsconfig.json src", + "lf": "bun .agents/scripts/check-lf.ts", + "lf:fix": "bun .agents/scripts/check-lf.ts --fix", "secretlint": "secretlint \"**/*\"", "lint-staged": "lint-staged", "prepare": "bun .husky/install.mjs", diff --git a/test/check-lf.test.ts b/test/check-lf.test.ts new file mode 100644 index 0000000..55e851f --- /dev/null +++ b/test/check-lf.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test"; +import { hasCarriageReturn, isBinary } from "../.agents/scripts/check-lf"; + +describe("hasCarriageReturn", () => { + test("flags a CRLF worktree even when the index is normalized", () => { + expect(hasCarriageReturn("i/lf w/crlf attr/text=auto eol=lf \tfile.txt")).toBe( + true + ); + }); + + test("flags a CRLF index", () => { + expect(hasCarriageReturn("i/crlf w/lf attr/ \tfile.txt")).toBe(true); + }); + + test("flags mixed endings", () => { + expect(hasCarriageReturn("i/mixed w/lf attr/ \tfile.txt")).toBe(true); + }); + + test("passes an all-LF file", () => { + expect(hasCarriageReturn("i/lf w/lf attr/text=auto eol=lf \tfile.txt")).toBe( + false + ); + }); + + test("passes a file git reports as binary", () => { + expect(hasCarriageReturn("i/-text w/-text attr/text \timage.png")).toBe(false); + }); +}); + +// git's own -text detection screens binaries out before the fix path sees them, +// so this guard exists for a .gitattributes override that forces `text` on +// binary content. It cannot be reached end-to-end, hence the direct test. +describe("isBinary", () => { + test("treats a NUL byte as binary", () => { + expect(isBinary(Buffer.from("PNG\x00\x01\x02\r\n"))).toBe(true); + }); + + test("treats CRLF text as convertible", () => { + expect(isBinary(Buffer.from("line one\r\nline two\r\n"))).toBe(false); + }); + + test("treats an empty file as convertible", () => { + expect(isBinary(Buffer.from(""))).toBe(false); + }); +}); From dd8d54749f6938e6354902b6dc7791100b609c2a Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 12:07:47 +0200 Subject: [PATCH 12/35] chore(stryker): make the mutation gate blocking at a 50 percent threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scope the ring to the modules the in-process suite can reach - record the ratchet rule for removing an exclusion and raising the threshold - set break to 50, one rung below the measured 54.87 percent baseline Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- stryker.config.mjs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/stryker.config.mjs b/stryker.config.mjs index d20c5a6..9b910aa 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -50,6 +50,24 @@ export default { "!src/hooks/lib/notify.ts", "!src/hooks/lib/stdin.ts", "!src/hooks/lib/which.ts", + // Ratchet — every entry below measured >=90% no-coverage on 2026-08-18, meaning + // the in-process suite cannot reach it and its mutants only depress the score. + // Delete an entry the same commit that gives the module in-process tests, then + // re-measure and raise thresholds.break. Entries come off this list; they never + // go back on, and break never moves down without a reason recorded here. + "!src/hooks/lib/import-merge.ts", + "!src/hooks/lib/learning-category.ts", + "!src/tools/agent/algorithm-reflect.ts", + "!src/tools/agent/analyze.ts", + "!src/tools/agent/handoff-note.ts", + "!src/tools/agent/project.ts", + "!src/tools/agent/relationship-note.ts", + "!src/tools/agent/thread.ts", + "!src/tools/relationship-reflect.ts", + "!src/tools/self-model.ts", + "!src/tools/session-summary.ts", + "!src/tools/skill-doctor.ts", + "!src/tools/token-cost.ts", ], concurrency: Number(process.env.STRYKER_CONCURRENCY ?? 4), bun: { @@ -61,11 +79,11 @@ export default { inspectorTimeout: 60000, }, reporters: ["clear-text", "progress", "html"], - // Measured 2026-08-18 over the whole ring: 10823 mutants, 37.43% total / 70.90% of - // covered, 0 errors. `break` stays null until the CLI-entrypoint tools under - // src/tools/ have in-process tests — a threshold guessed before that just gets - // disabled the first time it fires. The PR gate reports; it does not yet block. - thresholds: { high: 80, low: 60, break: null }, + // Ratchet rung 1, measured 2026-08-18 over the pruned ring: 7627 mutants, + // 54.87% total / 70.81% of covered, 0 errors. `break` sits one rung below the + // measurement so a normal change has headroom. Raise it only after a run beats + // the new number; lower it only with the reason written here. + thresholds: { high: 80, low: 60, break: 50 }, // Stryker copies the project into a sandbox with fs.copyFile, which throws ENOTSUP on a // symlink. Every entry below is either a symlink farm (agent config dirs, the installed // test homes, the vendored skill node_modules) or bulk the suite never reads. From 9591e404fa50135be1706beba32e06023d3eb490 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 12:19:30 +0200 Subject: [PATCH 13/35] test(targets): cover per-platform agent extraction and removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assert global fields survive for every platform - assert the target platform block is un-indented into the frontmatter root - assert other platform blocks are stripped - assert an agent with no block for a platform keeps only its global fields - assert body content and its horizontal rules survive extraction - cover install idempotence and removal leaving unrelated files alone Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/agent-extract.test.ts | 153 +++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 test/agent-extract.test.ts diff --git a/test/agent-extract.test.ts b/test/agent-extract.test.ts new file mode 100644 index 0000000..1679ea1 --- /dev/null +++ b/test/agent-extract.test.ts @@ -0,0 +1,153 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import { + copyAgentsForCopilot, + copyAgentsForCursor, + copyAgentsForOpencode, + removeAgentsFromCopilot, + removeAgentsFromCursor, + removeAgentsFromOpencode, +} from "../src/targets/lib"; + +const dirs: string[] = []; + +function tmp(): string { + const dir = mkdtempSync(resolve(tmpdir(), "pal-agents-")); + dirs.push(dir); + return dir; +} + +function agentFile(dir: string, name = "gemini-researcher.md"): string { + return readFileSync(resolve(dir, name), "utf-8"); +} + +afterEach(() => { + for (const dir of dirs.splice(0)) { + if (existsSync(dir)) rmSync(dir, { recursive: true }); + } +}); + +describe("agent extraction per platform", () => { + test("installs every shipped agent and reports the count", () => { + const dir = tmp(); + + const count = copyAgentsForOpencode(dir); + + expect(count).toBeGreaterThan(0); + expect(readdirSync(dir).filter((f) => f.endsWith(".md"))).toHaveLength(count); + }); + + test("keeps the global fields for every platform", () => { + const oc = tmp(); + const cur = tmp(); + copyAgentsForOpencode(oc); + copyAgentsForCursor(cur); + + for (const content of [agentFile(oc), agentFile(cur)]) { + expect(content).toContain("name: gemini-researcher"); + expect(content).toContain("description: Deep research"); + } + }); + + test("un-indents the target platform block into the frontmatter root", () => { + const dir = tmp(); + copyAgentsForOpencode(dir); + + const content = agentFile(dir); + + expect(content).toContain("\nmode: subagent"); + expect(content).toContain("\npermission:"); + expect(content).toContain("\n read: allow"); + }); + + test("strips every other platform's block", () => { + const dir = tmp(); + copyAgentsForCursor(dir); + + const content = agentFile(dir); + + expect(content).toContain("model: inherit"); + expect(content).not.toContain("mode: subagent"); + expect(content).not.toContain("tools: Bash, WebSearch"); + expect(content).not.toContain("opencode:"); + expect(content).not.toContain("claude:"); + expect(content).not.toContain("cursor:"); + }); + + test("keeps only the global fields when the agent has no block for that platform", () => { + const dir = tmp(); + copyAgentsForCopilot(dir); + + const content = agentFile(dir); + + expect(content).toContain("name: gemini-researcher"); + expect(content).not.toContain("model: sonnet"); + expect(content).not.toContain("mode: subagent"); + expect(content).not.toContain("model: inherit"); + }); + + test("preserves the body after the frontmatter", () => { + const dir = tmp(); + copyAgentsForOpencode(dir); + + const content = agentFile(dir); + const frontmatterEnd = content.indexOf("\n---", 3); + + expect(content.startsWith("---\n")).toBe(true); + expect(content).toContain("You are a research specialist"); + expect(content.indexOf("You are a research specialist")).toBeGreaterThan( + frontmatterEnd + ); + }); + + // The source bodies carry their own `---` horizontal rules, so the extractor + // splits into more than three parts and rejoins everything past the frontmatter. + test("keeps horizontal rules that appear inside the body", () => { + const dir = tmp(); + copyAgentsForOpencode(dir); + + const source = readFileSync("assets/agents/gemini-researcher.md", "utf-8"); + const sourceRules = source.split(/^---\s*$/m).length - 1; + + expect(agentFile(dir).split(/^---\s*$/m).length - 1).toBe(sourceRules); + }); + + test("overwrites an existing install rather than duplicating", () => { + const dir = tmp(); + const first = copyAgentsForOpencode(dir); + const second = copyAgentsForOpencode(dir); + + expect(second).toBe(first); + expect(readdirSync(dir).filter((f) => f.endsWith(".md"))).toHaveLength(first); + }); +}); + +describe("agent removal per platform", () => { + test("removes what it installed and names each one", () => { + const dir = tmp(); + const count = copyAgentsForOpencode(dir); + + const removed = removeAgentsFromOpencode(dir); + + expect(removed).toHaveLength(count); + expect(removed).toContain("gemini-researcher"); + expect(removed.some((n) => n.endsWith(".md"))).toBe(false); + expect(readdirSync(dir).filter((f) => f.endsWith(".md"))).toHaveLength(0); + }); + + test("reports nothing when there is nothing installed", () => { + expect(removeAgentsFromCursor(tmp())).toEqual([]); + }); + + test("leaves an unrelated file in the directory alone", () => { + const dir = tmp(); + copyAgentsForCopilot(dir); + Bun.write(resolve(dir, "mine.md"), "keep me"); + + removeAgentsFromCopilot(dir); + + expect(existsSync(resolve(dir, "mine.md"))).toBe(true); + }); +}); From 02377fcc40aa9de5f4b5712ff4e5bcd320babb82 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 12:27:29 +0200 Subject: [PATCH 14/35] refactor(targets): resolve installer paths per call instead of at import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - turn the docs, tools, skills, claude-agents, and personal-agents paths into getters - let a caller that sets PAL_HOME after import reach the intended directory Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/targets/lib.ts | 58 ++++++++++----------- test/targets-paths.test.ts | 103 +++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 29 deletions(-) create mode 100644 test/targets-paths.test.ts diff --git a/src/targets/lib.ts b/src/targets/lib.ts index 30dc28a..299287d 100644 --- a/src/targets/lib.ts +++ b/src/targets/lib.ts @@ -691,8 +691,8 @@ export function scaffoldPalSettings(): void { // --- PAL docs (modular context routing files) --- -const PAL_DOCS_DIR = resolve(palHome(), "docs"); -const PAL_TOOLS_DIR = resolve(palHome(), "tools"); +const palDocsDir = () => resolve(palHome(), "docs"); +const palToolsDir = () => resolve(palHome(), "tools"); /** * Install PAL system docs into ~/.pal/docs/. @@ -703,19 +703,19 @@ export function copyPalDocs(): number { const srcDir = assets.palDocs(); if (!existsSync(srcDir)) return 0; - mkdirSync(PAL_DOCS_DIR, { recursive: true }); + mkdirSync(palDocsDir(), { recursive: true }); let count = 0; for (const file of readdirSync(srcDir).filter((f) => f.endsWith(".md"))) { const src = resolve(srcDir, file); - const dst = resolve(PAL_DOCS_DIR, file); + const dst = resolve(palDocsDir(), file); copyFileSync(src, dst); count++; } // ~/.pal/tools/ → repo agent tools const linkType = process.platform === "win32" ? "junction" : "dir"; - ensureSymlink(PAL_TOOLS_DIR, assets.agentTools(), linkType); + ensureSymlink(palToolsDir(), assets.agentTools(), linkType); return count; } @@ -724,13 +724,13 @@ export function copyPalDocs(): number { export function removePalDocs(): void { // Remove tools symlink try { - unlinkSync(PAL_TOOLS_DIR); + unlinkSync(palToolsDir()); } catch { /* gone */ } - if (!existsSync(PAL_DOCS_DIR)) return; + if (!existsSync(palDocsDir())) return; try { - rmSync(PAL_DOCS_DIR, { recursive: true }); + rmSync(palDocsDir(), { recursive: true }); log.info("Removed ~/.pal/docs/"); } catch { /* gone */ @@ -739,7 +739,7 @@ export function removePalDocs(): void { // --- Skills --- -const PAL_SKILLS_DIR = resolve(palHome(), "skills"); +const palSkillsDir = () => resolve(palHome(), "skills"); /** * Run one step of a bulk install, naming it only when it fails. @@ -770,7 +770,7 @@ export function copySkills(claudeSkillsDir: string): number { const skillsDir = assets.skills(); if (!existsSync(skillsDir)) return 0; - mkdirSync(PAL_SKILLS_DIR, { recursive: true }); + mkdirSync(palSkillsDir(), { recursive: true }); mkdirSync(claudeSkillsDir, { recursive: true }); const linkType = process.platform === "win32" ? "junction" : "dir"; let count = 0; @@ -779,7 +779,7 @@ export function copySkills(claudeSkillsDir: string): number { const srcDir = resolve(skillsDir, name); if (!existsSync(resolve(srcDir, "SKILL.md"))) continue; - const palLink = resolve(PAL_SKILLS_DIR, name); + const palLink = resolve(palSkillsDir(), name); const claudeLink = resolve(claudeSkillsDir, name); const linked = reportOnlyOnFailure(`skill ${name}`, () => { // ~/.pal/skills/<name> → <repo>/assets/skills/<name> @@ -792,7 +792,7 @@ export function copySkills(claudeSkillsDir: string): number { // ~/.agents/skills/ → ~/.pal/skills/ mkdirSync(platform.agentsDir(), { recursive: true }); - ensureSymlink(resolve(platform.agentsDir(), "skills"), PAL_SKILLS_DIR, linkType); + ensureSymlink(resolve(platform.agentsDir(), "skills"), palSkillsDir(), linkType); return count; } @@ -823,7 +823,7 @@ function perSkillAgentDirs(): { agent: string; dir: string }[] { * is covered by the whole-dir ~/.agents/skills link). */ export function linkPersonalSkill(name: string): string[] { - const palLink = resolve(PAL_SKILLS_DIR, name); + const palLink = resolve(palSkillsDir(), name); if (!existsSync(resolve(palLink, "SKILL.md"))) { throw new Error(`No skill found at ${palLink}/SKILL.md`); } @@ -886,7 +886,7 @@ export function removeSkills(claudeSkillsDir: string): string[] { for (const name of readdirSync(skillsDir)) { if (!existsSync(resolve(skillsDir, name, "SKILL.md"))) continue; - for (const link of [resolve(PAL_SKILLS_DIR, name), resolve(claudeSkillsDir, name)]) { + for (const link of [resolve(palSkillsDir(), name), resolve(claudeSkillsDir, name)]) { try { unlinkSync(link); } catch { @@ -909,14 +909,14 @@ export function removeSkills(claudeSkillsDir: string): string[] { // --- Agents --- -const CLAUDE_AGENTS_DIR = resolve(platform.claudeDir(), "agents"); +const claudeAgentsDir = () => resolve(platform.claudeDir(), "agents"); /** * Install PAL agent definitions into ~/.claude/agents/. * Always overwrites — engine-managed, not user-editable. */ export function copyAgents(): number { - return installAgents(CLAUDE_AGENTS_DIR, "claude"); + return installAgents(claudeAgentsDir(), "claude"); } /** Remove PAL agents from ~/.claude/agents/ */ @@ -926,7 +926,7 @@ export function removeAgents(): string[] { const removed: string[] = []; for (const file of readdirSync(agentsDir).filter((f) => f.endsWith(".md"))) { - const dst = resolve(CLAUDE_AGENTS_DIR, file); + const dst = resolve(claudeAgentsDir(), file); if (existsSync(dst)) { unlinkSync(dst); const name = file.replace(/\.md$/, ""); @@ -939,9 +939,9 @@ export function removeAgents(): string[] { /** Count agent .md files in ~/.claude/agents/ */ export function countAgents(): number { - if (!existsSync(CLAUDE_AGENTS_DIR)) return 0; + if (!existsSync(claudeAgentsDir())) return 0; try { - return readdirSync(CLAUDE_AGENTS_DIR).filter((f) => f.endsWith(".md")).length; + return readdirSync(claudeAgentsDir()).filter((f) => f.endsWith(".md")).length; } catch { return 0; } @@ -1077,7 +1077,7 @@ export function removeAgentsFromCopilot(copilotAgentsDir: string): string[] { * Store for user-authored subagents: ~/.pal/agents/<name>.md — one merged * multi-platform frontmatter file per subagent (same schema as assets/agents/). */ -const PAL_AGENTS_STORE = resolve(palHome(), "agents"); +const palAgentsStore = () => resolve(palHome(), "agents"); /** * Each installed agent and the native agents directory a personal subagent is @@ -1106,8 +1106,8 @@ function shippedAgentNames(): Set<string> { /** List the user-authored subagents in ~/.pal/agents/. */ export function listPersonalSubagents(): string[] { - if (!existsSync(PAL_AGENTS_STORE)) return []; - return readdirSync(PAL_AGENTS_STORE) + if (!existsSync(palAgentsStore())) return []; + return readdirSync(palAgentsStore()) .filter((f) => f.endsWith(".md")) .map((f) => f.replace(/\.md$/, "")) .sort(); @@ -1121,7 +1121,7 @@ export function listPersonalSubagents(): string[] { * its own frontmatter shape. Returns the agents it was installed into. */ export function installPersonalSubagent(name: string): string[] { - const src = resolve(PAL_AGENTS_STORE, `${name}.md`); + const src = resolve(palAgentsStore(), `${name}.md`); if (!existsSync(src)) { throw new Error(`No subagent found at ${src}`); } @@ -1353,7 +1353,7 @@ function extractTriggers(description: string): string[] { * Called during install after skills are symlinked. */ export function generateSkillIndex(): number { - if (!existsSync(PAL_SKILLS_DIR)) return 0; + if (!existsSync(palSkillsDir())) return 0; const index: SkillIndex = { generated: new Date().toISOString(), @@ -1361,8 +1361,8 @@ export function generateSkillIndex(): number { skills: {}, }; - for (const name of readdirSync(PAL_SKILLS_DIR)) { - const skillMd = resolve(PAL_SKILLS_DIR, name, "SKILL.md"); + for (const name of readdirSync(palSkillsDir())) { + const skillMd = resolve(palSkillsDir(), name, "SKILL.md"); if (!existsSync(skillMd)) continue; try { @@ -1399,10 +1399,10 @@ export function generateSkillIndex(): number { /** Count skill subdirectories in ~/.pal/skills/ */ export function countSkills(): number { - if (!existsSync(PAL_SKILLS_DIR)) return 0; + if (!existsSync(palSkillsDir())) return 0; try { - return readdirSync(PAL_SKILLS_DIR).filter((f) => - existsSync(resolve(PAL_SKILLS_DIR, f, "SKILL.md")) + return readdirSync(palSkillsDir()).filter((f) => + existsSync(resolve(palSkillsDir(), f, "SKILL.md")) ).length; } catch { return 0; diff --git a/test/targets-paths.test.ts b/test/targets-paths.test.ts new file mode 100644 index 0000000..d4608f7 --- /dev/null +++ b/test/targets-paths.test.ts @@ -0,0 +1,103 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +// Imported at file load, before PAL_HOME is set below: these functions resolve +// their directories per call, so the env set in beforeEach still takes effect. +import { + countAgents, + countMd, + countSkills, + listPersonalSubagents, +} from "../src/targets/lib"; + +const HOME = resolve(import.meta.dir, "../.test-home-targets-paths"); +const CLAUDE = resolve(HOME, ".claude"); +const savedHome = process.env.PAL_HOME; +const savedClaude = process.env.PAL_CLAUDE_DIR; + +function skill(name: string, withManifest = true) { + mkdirSync(resolve(HOME, "skills", name), { recursive: true }); + if (withManifest) writeFileSync(resolve(HOME, "skills", name, "SKILL.md"), "x"); +} + +beforeEach(() => { + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); + process.env.PAL_HOME = HOME; + process.env.PAL_CLAUDE_DIR = CLAUDE; +}); + +afterEach(() => { + if (savedHome === undefined) delete process.env.PAL_HOME; + else process.env.PAL_HOME = savedHome; + if (savedClaude === undefined) delete process.env.PAL_CLAUDE_DIR; + else process.env.PAL_CLAUDE_DIR = savedClaude; + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); +}); + +describe("countSkills", () => { + test("counts only directories holding a SKILL.md", () => { + skill("real-one"); + skill("also-real"); + skill("no-manifest", false); + + expect(countSkills()).toBe(2); + }); + + test("returns 0 when the skills directory is absent", () => { + expect(countSkills()).toBe(0); + }); + + test("returns 0 for an empty skills directory", () => { + mkdirSync(resolve(HOME, "skills"), { recursive: true }); + + expect(countSkills()).toBe(0); + }); +}); + +describe("countMd", () => { + test("counts only .md files", () => { + const dir = resolve(HOME, "docs"); + mkdirSync(dir, { recursive: true }); + writeFileSync(resolve(dir, "one.md"), "x"); + writeFileSync(resolve(dir, "two.md"), "x"); + writeFileSync(resolve(dir, "notes.txt"), "x"); + + expect(countMd(dir)).toBe(2); + }); + + test("returns 0 for a missing directory", () => { + expect(countMd(resolve(HOME, "nope"))).toBe(0); + }); +}); + +describe("countAgents", () => { + test("counts .md files in the claude agents directory", () => { + const dir = resolve(CLAUDE, "agents"); + mkdirSync(dir, { recursive: true }); + writeFileSync(resolve(dir, "a.md"), "x"); + writeFileSync(resolve(dir, "b.md"), "x"); + writeFileSync(resolve(dir, "ignored.json"), "x"); + + expect(countAgents()).toBe(2); + }); + + test("returns 0 when the agents directory is absent", () => { + expect(countAgents()).toBe(0); + }); +}); + +describe("listPersonalSubagents", () => { + test("lists agent names without the .md suffix", () => { + mkdirSync(resolve(HOME, "agents"), { recursive: true }); + writeFileSync(resolve(HOME, "agents", "helper.md"), "x"); + writeFileSync(resolve(HOME, "agents", "other.md"), "x"); + writeFileSync(resolve(HOME, "agents", "README.txt"), "x"); + + expect(listPersonalSubagents().sort()).toEqual(["helper", "other"]); + }); + + test("returns an empty list when the store is absent", () => { + expect(listPersonalSubagents()).toEqual([]); + }); +}); From cec9f15715fc4ee2379154b5a2a889d63695498d Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 13:04:03 +0200 Subject: [PATCH 15/35] test(targets): cover the skill index and personal skill linking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assert the index records name, description, and generation timestamp - assert skills without frontmatter or without a name are skipped - assert quoted descriptions are unwrapped - cover trigger extraction: use-when keywords, stopword removal, domain terms, dedup - assert linkPersonalSkill throws for an unknown skill and links only installed agents - assert the link is a symlink and re-linking stays idempotent Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/skill-index.test.ts | 195 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 test/skill-index.test.ts diff --git a/test/skill-index.test.ts b/test/skill-index.test.ts new file mode 100644 index 0000000..1f56e6e --- /dev/null +++ b/test/skill-index.test.ts @@ -0,0 +1,195 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { + existsSync, + lstatSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { resolve } from "node:path"; +import { generateSkillIndex, linkPersonalSkill } from "../src/targets/lib"; + +const HOME = resolve(import.meta.dir, "../.test-home-skill-index"); +const AGENT_DIRS = { + PAL_CLAUDE_DIR: resolve(HOME, ".claude"), + PAL_CURSOR_DIR: resolve(HOME, ".cursor"), + PAL_COPILOT_DIR: resolve(HOME, ".copilot"), + PAL_CODEX_DIR: resolve(HOME, ".codex"), + PAL_AGENTS_DIR: resolve(HOME, ".agents"), +}; +const saved: Record<string, string | undefined> = {}; + +function writeSkill(name: string, frontmatter: string | null, body = "text") { + const dir = resolve(HOME, "skills", name); + mkdirSync(dir, { recursive: true }); + const content = frontmatter === null ? body : `---\n${frontmatter}\n---\n${body}`; + writeFileSync(resolve(dir, "SKILL.md"), content); + return dir; +} + +function readIndex() { + return JSON.parse( + readFileSync(resolve(HOME, "memory", "state", "skill-index.json"), "utf-8") + ); +} + +beforeEach(() => { + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); + saved.PAL_HOME = process.env.PAL_HOME; + process.env.PAL_HOME = HOME; + for (const [key, value] of Object.entries(AGENT_DIRS)) { + saved[key] = process.env[key]; + process.env[key] = value; + } +}); + +afterEach(() => { + for (const key of ["PAL_HOME", ...Object.keys(AGENT_DIRS)]) { + if (saved[key] === undefined) delete process.env[key]; + else process.env[key] = saved[key]; + } + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); +}); + +describe("generateSkillIndex", () => { + test("returns 0 and writes nothing when there is no skills directory", () => { + expect(generateSkillIndex()).toBe(0); + expect(existsSync(resolve(HOME, "memory", "state", "skill-index.json"))).toBe(false); + }); + + test("indexes a skill by its frontmatter name and description", () => { + writeSkill("alpha", "name: alpha\ndescription: Does a thing."); + + expect(generateSkillIndex()).toBe(1); + const index = readIndex(); + expect(index.totalSkills).toBe(1); + expect(index.skills.alpha.name).toBe("alpha"); + expect(index.skills.alpha.description).toBe("Does a thing."); + expect(index.generated).toMatch(/^\d{4}-\d{2}-\d{2}T/); + }); + + test("skips a skill with no frontmatter", () => { + writeSkill("bare", null); + + expect(generateSkillIndex()).toBe(0); + }); + + test("skips a skill whose frontmatter has no name", () => { + writeSkill("nameless", "description: Has no name field."); + + expect(generateSkillIndex()).toBe(0); + }); + + test("indexes a named skill that has no description", () => { + writeSkill("terse", "name: terse"); + + expect(generateSkillIndex()).toBe(1); + expect(readIndex().skills.terse.description).toBe(""); + }); + + test("strips surrounding quotes from the description", () => { + writeSkill("quoted", 'name: quoted\ndescription: "Quoted text."'); + + expect(readIndexAfterGenerate().skills.quoted.description).toBe("Quoted text."); + }); + + test("counts every valid skill", () => { + writeSkill("one", "name: one"); + writeSkill("two", "name: two"); + writeSkill("three", "name: three"); + + expect(generateSkillIndex()).toBe(3); + }); +}); + +function readIndexAfterGenerate() { + generateSkillIndex(); + return readIndex(); +} + +describe("generateSkillIndex — trigger extraction", () => { + function triggersFor(description: string): string[] { + writeSkill("probe", `name: probe\ndescription: ${description}`); + generateSkillIndex(); + return readIndex().skills.probe.triggers; + } + + test("pulls keywords out of a 'Use when' clause", () => { + expect(triggersFor("Use when scheduling deployments.")).toContain("scheduling"); + expect(triggersFor("Use when scheduling deployments.")).toContain("deployments"); + }); + + test("drops stopwords and short words from the clause", () => { + const triggers = triggersFor("Use when the user wants that with your team."); + + for (const dropped of ["the", "that", "with", "your", "when"]) { + expect(triggers).not.toContain(dropped); + } + }); + + test("picks up domain terms from anywhere in the description", () => { + const triggers = triggersFor("Handles security auditing and pdf output."); + + expect(triggers).toContain("security"); + expect(triggers).toContain("pdf"); + }); + + test("does not repeat a term that appears twice", () => { + const triggers = triggersFor("Use when research is needed. Deep research here."); + + expect(triggers.filter((t) => t === "research")).toHaveLength(1); + }); + + test("yields no triggers for a description with neither pattern", () => { + expect(triggersFor("Nondescript helper.")).toEqual([]); + }); +}); + +describe("linkPersonalSkill", () => { + test("throws when the skill is not in the PAL store", () => { + expect(() => linkPersonalSkill("ghost")).toThrow("No skill found"); + }); + + test("links only into agents whose skills directory exists", () => { + writeSkill("mine", "name: mine"); + mkdirSync(resolve(AGENT_DIRS.PAL_CLAUDE_DIR, "skills"), { recursive: true }); + mkdirSync(resolve(AGENT_DIRS.PAL_CODEX_DIR, "skills"), { recursive: true }); + + const linked = linkPersonalSkill("mine"); + + expect(linked.sort()).toEqual(["claude", "codex"]); + expect(existsSync(resolve(AGENT_DIRS.PAL_CURSOR_DIR, "skills", "mine"))).toBe(false); + }); + + test("creates a symlink rather than a copy", () => { + writeSkill("mine", "name: mine"); + mkdirSync(resolve(AGENT_DIRS.PAL_CLAUDE_DIR, "skills"), { recursive: true }); + + linkPersonalSkill("mine"); + + const link = resolve(AGENT_DIRS.PAL_CLAUDE_DIR, "skills", "mine"); + expect(lstatSync(link).isSymbolicLink()).toBe(true); + expect(existsSync(resolve(link, "SKILL.md"))).toBe(true); + }); + + test("returns an empty list when no agent is installed", () => { + writeSkill("mine", "name: mine"); + + expect(linkPersonalSkill("mine")).toEqual([]); + }); + + test("is idempotent — re-linking leaves one symlink", () => { + writeSkill("mine", "name: mine"); + mkdirSync(resolve(AGENT_DIRS.PAL_CLAUDE_DIR, "skills"), { recursive: true }); + + linkPersonalSkill("mine"); + const second = linkPersonalSkill("mine"); + + expect(second).toEqual(["claude"]); + expect( + lstatSync(resolve(AGENT_DIRS.PAL_CLAUDE_DIR, "skills", "mine")).isSymbolicLink() + ).toBe(true); + }); +}); From 91cfeb73cede70b27ed6c4e814d55df3086fb5ce Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 13:36:04 +0200 Subject: [PATCH 16/35] test(targets): cover telos, settings, docs, statusline, and skill installation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assert scaffolding creates from templates and preserves user edits - assert deprecated loadAtStartup entries are stripped and valid ones kept - assert malformed settings are left as-is - assert docs install with a tools symlink and both are removed together - assert the statusline installs per agent target and removes only that target - assert shipped skills link into the PAL store, the agent dir, and the agents skills link - assert skill installation is idempotent and removal reports each name Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/targets-install.test.ts | 224 +++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 test/targets-install.test.ts diff --git a/test/targets-install.test.ts b/test/targets-install.test.ts new file mode 100644 index 0000000..1387392 --- /dev/null +++ b/test/targets-install.test.ts @@ -0,0 +1,224 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { + existsSync, + lstatSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { resolve } from "node:path"; +import { + copyPalDocs, + copySkills, + copyStatusline, + removePalDocs, + removeSkills, + removeStatusline, + scaffoldPalSettings, + scaffoldTelos, +} from "../src/targets/lib"; + +const HOME = resolve(import.meta.dir, "../.test-home-targets-install"); +// copySkills also symlinks into platform.agentsDir(), so PAL_AGENTS_DIR must be +// redirected too or the test writes into the developer's real ~/.agents. +const ENV = { + PAL_HOME: HOME, + PAL_CLAUDE_DIR: resolve(HOME, ".claude"), + PAL_CURSOR_DIR: resolve(HOME, ".cursor"), + PAL_COPILOT_DIR: resolve(HOME, ".copilot"), + PAL_CODEX_DIR: resolve(HOME, ".codex"), + PAL_AGENTS_DIR: resolve(HOME, ".agents"), +}; +const saved: Record<string, string | undefined> = {}; +const SCRIPT = process.platform === "win32" ? "statusline.ps1" : "statusline.sh"; + +beforeEach(() => { + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); + for (const [key, value] of Object.entries(ENV)) { + saved[key] = process.env[key]; + process.env[key] = value; + } +}); + +afterEach(() => { + for (const key of Object.keys(ENV)) { + if (saved[key] === undefined) delete process.env[key]; + else process.env[key] = saved[key]; + } + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); +}); + +describe("scaffoldTelos", () => { + test("creates the telos directory from the shipped templates", () => { + scaffoldTelos(); + + const telos = resolve(HOME, "telos"); + expect(existsSync(telos)).toBe(true); + expect(readdirSync(telos).filter((f) => f.endsWith(".md")).length).toBeGreaterThan(0); + }); + + test("never overwrites a file the user already edited", () => { + scaffoldTelos(); + const first = readdirSync(resolve(HOME, "telos")).find((f) => f.endsWith(".md")); + const target = resolve(HOME, "telos", first as string); + writeFileSync(target, "my own words"); + + scaffoldTelos(); + + expect(readFileSync(target, "utf-8")).toBe("my own words"); + }); +}); + +describe("scaffoldPalSettings", () => { + test("creates pal-settings.json from the template", () => { + scaffoldPalSettings(); + + expect(existsSync(resolve(HOME, "memory", "pal-settings.json"))).toBe(true); + }); + + test("leaves an existing settings file's own keys intact", () => { + mkdirSync(resolve(HOME, "memory"), { recursive: true }); + const dst = resolve(HOME, "memory", "pal-settings.json"); + writeFileSync(dst, JSON.stringify({ mine: true })); + + scaffoldPalSettings(); + + expect(JSON.parse(readFileSync(dst, "utf-8")).mine).toBe(true); + }); + + test("strips a deprecated PROJECTS.md entry from loadAtStartup.files", () => { + mkdirSync(resolve(HOME, "memory"), { recursive: true }); + const dst = resolve(HOME, "memory", "pal-settings.json"); + writeFileSync( + dst, + JSON.stringify({ + loadAtStartup: { files: ["telos/GOALS.md", "memory/PROJECTS.md"] }, + }) + ); + + scaffoldPalSettings(); + + expect(JSON.parse(readFileSync(dst, "utf-8")).loadAtStartup.files).toEqual([ + "telos/GOALS.md", + ]); + }); + + test("leaves loadAtStartup alone when nothing is deprecated", () => { + mkdirSync(resolve(HOME, "memory"), { recursive: true }); + const dst = resolve(HOME, "memory", "pal-settings.json"); + const files = ["telos/GOALS.md", "telos/MISSION.md"]; + writeFileSync(dst, JSON.stringify({ loadAtStartup: { files } })); + + scaffoldPalSettings(); + + expect(JSON.parse(readFileSync(dst, "utf-8")).loadAtStartup.files).toEqual(files); + }); + + test("survives a malformed settings file", () => { + mkdirSync(resolve(HOME, "memory"), { recursive: true }); + const dst = resolve(HOME, "memory", "pal-settings.json"); + writeFileSync(dst, "{ not json"); + + expect(() => scaffoldPalSettings()).not.toThrow(); + expect(readFileSync(dst, "utf-8")).toBe("{ not json"); + }); +}); + +describe("PAL docs", () => { + test("copies the shipped docs and links the agent tools", () => { + const count = copyPalDocs(); + + expect(count).toBeGreaterThan(0); + expect(readdirSync(resolve(HOME, "docs")).length).toBe(count); + expect(lstatSync(resolve(HOME, "tools")).isSymbolicLink()).toBe(true); + }); + + test("removes both the docs directory and the tools link", () => { + copyPalDocs(); + + removePalDocs(); + + expect(existsSync(resolve(HOME, "docs"))).toBe(false); + expect(existsSync(resolve(HOME, "tools"))).toBe(false); + }); + + test("removing when nothing was installed is harmless", () => { + expect(() => removePalDocs()).not.toThrow(); + }); +}); + +describe("statusline install", () => { + test("installs the script into the claude directory", () => { + expect(copyStatusline("claude")).toBe(true); + expect(existsSync(resolve(ENV.PAL_CLAUDE_DIR, SCRIPT))).toBe(true); + }); + + test("installs into the cursor directory when targeted", () => { + expect(copyStatusline("cursor")).toBe(true); + expect(existsSync(resolve(ENV.PAL_CURSOR_DIR, SCRIPT))).toBe(true); + expect(existsSync(resolve(ENV.PAL_CLAUDE_DIR, SCRIPT))).toBe(false); + }); + + test("defaults to the claude target", () => { + copyStatusline(); + + expect(existsSync(resolve(ENV.PAL_CLAUDE_DIR, SCRIPT))).toBe(true); + }); + + test("removes only the targeted agent's script", () => { + copyStatusline("claude"); + copyStatusline("cursor"); + + expect(removeStatusline("cursor")).toBe(true); + + expect(existsSync(resolve(ENV.PAL_CURSOR_DIR, SCRIPT))).toBe(false); + expect(existsSync(resolve(ENV.PAL_CLAUDE_DIR, SCRIPT))).toBe(true); + }); + + test("reports success when there is nothing to remove", () => { + expect(removeStatusline("claude")).toBe(true); + }); +}); + +describe("shipped skills", () => { + test("links every shipped skill into both the PAL store and the agent dir", () => { + const claudeSkills = resolve(ENV.PAL_CLAUDE_DIR, "skills"); + + const count = copySkills(claudeSkills); + + expect(count).toBeGreaterThan(0); + expect(readdirSync(resolve(HOME, "skills"))).toHaveLength(count); + expect(readdirSync(claudeSkills)).toHaveLength(count); + expect( + lstatSync(resolve(claudeSkills, readdirSync(claudeSkills)[0])).isSymbolicLink() + ).toBe(true); + }); + + test("links the agents skills directory at the PAL store", () => { + copySkills(resolve(ENV.PAL_CLAUDE_DIR, "skills")); + + expect(lstatSync(resolve(ENV.PAL_AGENTS_DIR, "skills")).isSymbolicLink()).toBe(true); + }); + + test("is idempotent across repeated installs", () => { + const claudeSkills = resolve(ENV.PAL_CLAUDE_DIR, "skills"); + const first = copySkills(claudeSkills); + + expect(copySkills(claudeSkills)).toBe(first); + expect(readdirSync(claudeSkills)).toHaveLength(first); + }); + + test("removes what it linked and names each skill", () => { + const claudeSkills = resolve(ENV.PAL_CLAUDE_DIR, "skills"); + const count = copySkills(claudeSkills); + + const removed = removeSkills(claudeSkills); + + expect(removed).toHaveLength(count); + expect(readdirSync(claudeSkills)).toHaveLength(0); + expect(readdirSync(resolve(HOME, "skills"))).toHaveLength(0); + }); +}); From de135e8f3b6739572543a10d510085fec54f63d3 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 13:44:21 +0200 Subject: [PATCH 17/35] chore(stryker): raise the mutation threshold to 54 percent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - record rung 2 of the ratchet at a measured 58.77 percent ring score Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- stryker.config.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/stryker.config.mjs b/stryker.config.mjs index 9b910aa..13421b8 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -79,11 +79,13 @@ export default { inspectorTimeout: 60000, }, reporters: ["clear-text", "progress", "html"], - // Ratchet rung 1, measured 2026-08-18 over the pruned ring: 7627 mutants, - // 54.87% total / 70.81% of covered, 0 errors. `break` sits one rung below the - // measurement so a normal change has headroom. Raise it only after a run beats - // the new number; lower it only with the reason written here. - thresholds: { high: 80, low: 60, break: 50 }, + // Ratchet. `break` sits ~5 points below the last measured run so a normal change + // has headroom. Raise it only after a run beats the current number; lower it only + // with the reason written here. + // rung 1 — 2026-08-18: 7627 mutants, 54.87% total / 70.81% covered -> break 50 + // rung 2 — 2026-08-18: 7627 mutants, 58.77% total / 70.74% covered -> break 54 + // (src/targets/lib.ts 16.45% -> 63.67%, no-coverage 875 -> 183) + thresholds: { high: 80, low: 60, break: 54 }, // Stryker copies the project into a sandbox with fs.copyFile, which throws ENOTSUP on a // symlink. Every entry below is either a symlink farm (agent config dirs, the installed // test homes, the vendored skill node_modules) or bulk the suite never reads. From 7114c2b372c310c59498231bdfdf54da3cf8c225 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 13:50:01 +0200 Subject: [PATCH 18/35] test(hooks): cover relationship note storage and recall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assert the dated header appears once and each append gets an HH:MM section - assert opinion notes carry their confidence and omit it when absent - assert the session id and cwd are recorded only when an id is given - assert duplicate notes are skipped case-insensitively while fresh ones land - assert recall joins days with a rule, honours the day window, and skips empties - assert recall ignores non-month directories and non-markdown files Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/relationship.test.ts | 224 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 test/relationship.test.ts diff --git a/test/relationship.test.ts b/test/relationship.test.ts new file mode 100644 index 0000000..86e7208 --- /dev/null +++ b/test/relationship.test.ts @@ -0,0 +1,224 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { appendNotes, loadRecentNotes } from "../src/hooks/lib/relationship"; + +const HOME = resolve(import.meta.dir, "../.test-home-relationship"); +const savedHome = process.env.PAL_HOME; + +function ymd(offsetDays = 0): { month: string; day: string } { + const d = new Date(); + d.setDate(d.getDate() - offsetDays); + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return { month: `${yyyy}-${mm}`, day: `${yyyy}-${mm}-${dd}` }; +} + +function relDir(): string { + return resolve(HOME, "memory", "relationship"); +} + +function todayFile(): string { + const { month, day } = ymd(); + return resolve(relDir(), month, `${day}.md`); +} + +function seed(month: string, day: string, content: string) { + mkdirSync(resolve(relDir(), month), { recursive: true }); + writeFileSync(resolve(relDir(), month, `${day}.md`), content); +} + +beforeEach(() => { + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); + process.env.PAL_HOME = HOME; +}); + +afterEach(() => { + if (savedHome === undefined) delete process.env.PAL_HOME; + else process.env.PAL_HOME = savedHome; + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); +}); + +describe("appendNotes", () => { + test("writes nothing when given no notes", () => { + appendNotes([]); + + expect(existsSync(todayFile())).toBe(false); + }); + + test("creates today's file under memory/relationship/YYYY-MM/", () => { + appendNotes([{ type: "W", text: "uses bun" }]); + + expect(existsSync(todayFile())).toBe(true); + expect(readFileSync(todayFile(), "utf-8")).toContain("- W: uses bun"); + }); + + test("writes a dated header on the first append only", () => { + const { day } = ymd(); + + appendNotes([{ type: "W", text: "first fact" }]); + appendNotes([{ type: "W", text: "second fact" }]); + + const content = readFileSync(todayFile(), "utf-8"); + expect(content.match(new RegExp(`# Relationship Notes — ${day}`, "g"))).toHaveLength( + 1 + ); + expect(content).toContain("- W: first fact"); + expect(content).toContain("- W: second fact"); + }); + + test("stamps each append with an HH:MM section heading", () => { + appendNotes([{ type: "Session", text: "did a thing" }]); + + expect(readFileSync(todayFile(), "utf-8")).toMatch(/^## \d{2}:\d{2}$/m); + }); + + test("records the confidence of an opinion note", () => { + appendNotes([{ type: "O", text: "prefers terse replies", confidence: 0.8 }]); + + expect(readFileSync(todayFile(), "utf-8")).toContain( + "- O(c=0.8): prefers terse replies" + ); + }); + + test("omits the confidence marker when an opinion has none", () => { + appendNotes([{ type: "O", text: "prefers terse replies" }]); + + const content = readFileSync(todayFile(), "utf-8"); + expect(content).toContain("- O: prefers terse replies"); + expect(content).not.toContain("c="); + }); + + test("embeds the session id and cwd when given one", () => { + appendNotes([{ type: "W", text: "a fact" }], "sess-123"); + + const content = readFileSync(todayFile(), "utf-8"); + expect(content).toContain("<!-- session:sess-123"); + expect(content).toContain(`cwd:${process.cwd()}`); + }); + + test("omits the session comment when no id is given", () => { + appendNotes([{ type: "W", text: "a fact" }]); + + expect(readFileSync(todayFile(), "utf-8")).not.toContain("<!-- session:"); + }); + + test("skips a note whose text is already present", () => { + appendNotes([{ type: "W", text: "duplicated fact" }]); + appendNotes([{ type: "W", text: "duplicated fact" }]); + + const content = readFileSync(todayFile(), "utf-8"); + expect(content.match(/duplicated fact/g)).toHaveLength(1); + }); + + test("deduplicates regardless of letter case", () => { + appendNotes([{ type: "W", text: "Mixed Case Fact" }]); + appendNotes([{ type: "W", text: "mixed case fact" }]); + + expect( + readFileSync(todayFile(), "utf-8") + .toLowerCase() + .match(/mixed case fact/g) + ).toHaveLength(1); + }); + + test("keeps a fresh note that arrives alongside a duplicate", () => { + appendNotes([{ type: "W", text: "already known" }]); + appendNotes([ + { type: "W", text: "already known" }, + { type: "W", text: "brand new" }, + ]); + + const content = readFileSync(todayFile(), "utf-8"); + expect(content.match(/already known/g)).toHaveLength(1); + expect(content).toContain("- W: brand new"); + }); + + test("adds no new section when every note is a duplicate", () => { + appendNotes([{ type: "W", text: "only fact" }]); + const before = readFileSync(todayFile(), "utf-8"); + + appendNotes([{ type: "W", text: "only fact" }]); + + expect(readFileSync(todayFile(), "utf-8")).toBe(before); + }); +}); + +describe("loadRecentNotes", () => { + test("returns empty when nothing has been recorded", () => { + expect(loadRecentNotes()).toBe(""); + }); + + test("returns today's notes", () => { + const { month, day } = ymd(); + seed(month, day, "# Today\n- W: fresh"); + + expect(loadRecentNotes()).toContain("- W: fresh"); + }); + + test("joins several days with a horizontal rule", () => { + const today = ymd(); + const yesterday = ymd(1); + seed(today.month, today.day, "TODAY"); + seed(yesterday.month, yesterday.day, "YESTERDAY"); + + const loaded = loadRecentNotes(2); + + expect(loaded).toContain("TODAY"); + expect(loaded).toContain("YESTERDAY"); + expect(loaded).toContain("\n\n---\n\n"); + }); + + test("excludes a day older than the requested window", () => { + const today = ymd(); + const old = ymd(10); + seed(today.month, today.day, "RECENT"); + seed(old.month, old.day, "ANCIENT"); + + const loaded = loadRecentNotes(2); + + expect(loaded).toContain("RECENT"); + expect(loaded).not.toContain("ANCIENT"); + }); + + test("widens the window when asked for more days", () => { + const old = ymd(5); + seed(old.month, old.day, "FIVE-DAYS-BACK"); + + expect(loadRecentNotes(2)).not.toContain("FIVE-DAYS-BACK"); + expect(loadRecentNotes(30)).toContain("FIVE-DAYS-BACK"); + }); + + test("ignores a directory that is not a YYYY-MM month", () => { + const { month, day } = ymd(); + seed(month, day, "REAL"); + mkdirSync(resolve(relDir(), "notes-archive"), { recursive: true }); + writeFileSync(resolve(relDir(), "notes-archive", `${day}.md`), "STRAY"); + + const loaded = loadRecentNotes(); + + expect(loaded).toContain("REAL"); + expect(loaded).not.toContain("STRAY"); + }); + + test("ignores non-markdown files in a month directory", () => { + const { month, day } = ymd(); + seed(month, day, "REAL"); + writeFileSync(resolve(relDir(), month, "notes.txt"), "STRAY"); + + expect(loadRecentNotes()).not.toContain("STRAY"); + }); + + test("skips an empty note file", () => { + const today = ymd(); + const yesterday = ymd(1); + seed(today.month, today.day, " "); + seed(yesterday.month, yesterday.day, "HAS CONTENT"); + + const loaded = loadRecentNotes(2); + + expect(loaded).toBe("HAS CONTENT"); + }); +}); From 55f80ca70f7b718eebe574bfc047db8cee5a2976 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 13:55:06 +0200 Subject: [PATCH 19/35] test(tools): cover synthesis of ratings, reflections, and sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assert rating averages, low counts, and the ten-item recent window - assert the trend classifies as improving, declining, or stable - assert reflection criteria totals, pass rate, and the three-observation tail - assert session titles parse from frontmatter, a bold marker, or the filename - assert sessions group by date newest first and honour the day window - assert writeSynthesis persists the state and derives the signal cache windows Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/synthesize.test.ts | 353 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 test/synthesize.test.ts diff --git a/test/synthesize.test.ts b/test/synthesize.test.ts new file mode 100644 index 0000000..a2e5b6a --- /dev/null +++ b/test/synthesize.test.ts @@ -0,0 +1,353 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { synthesize, writeSynthesis } from "../src/tools/agent/synthesize"; + +const HOME = resolve(import.meta.dir, "../.test-home-synthesize"); +const savedHome = process.env.PAL_HOME; + +function iso(daysAgo: number): string { + return new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000).toISOString(); +} + +function writeRatings(rows: { ts: string; rating: number }[]) { + const dir = resolve(HOME, "memory", "signals"); + mkdirSync(dir, { recursive: true }); + writeFileSync( + resolve(dir, "ratings.jsonl"), + `${rows.map((r) => JSON.stringify(r)).join("\n")}\n` + ); +} + +function writeReflections(rows: Record<string, unknown>[]) { + const dir = resolve(HOME, "memory", "learning", "reflections"); + mkdirSync(dir, { recursive: true }); + writeFileSync( + resolve(dir, "algorithm-reflections.jsonl"), + `${rows.map((r) => JSON.stringify(r)).join("\n")}\n` + ); +} + +function reflection(over: Record<string, unknown> = {}) { + return { + timestamp: iso(1), + task: "a task", + criteria_count: 4, + criteria_passed: 4, + criteria_failed: 0, + sentiment: 8, + q1: "an observation", + ...over, + }; +} + +function writeSession(dateYmd: string, body: string) { + const year = dateYmd.slice(0, 4); + const month = dateYmd.slice(4, 6); + const dir = resolve(HOME, "memory", "learning", "session", year, month); + mkdirSync(dir, { recursive: true }); + writeFileSync(resolve(dir, `${dateYmd}-run.md`), body); +} + +function ymd(daysAgo = 0): string { + return iso(daysAgo).slice(0, 10).replace(/-/g, ""); +} + +beforeEach(() => { + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); + process.env.PAL_HOME = HOME; +}); + +afterEach(() => { + if (savedHome === undefined) delete process.env.PAL_HOME; + else process.env.PAL_HOME = savedHome; + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); +}); + +describe("synthesize — ratings", () => { + test("reports zeros and a stable trend with no ratings", () => { + expect(synthesize(7).ratings).toEqual({ + count: 0, + avg: 0, + recentAvg: 0, + lowCount: 0, + trend: "stable", + }); + }); + + test("averages the ratings inside the window and rounds to one decimal", () => { + writeRatings([ + { ts: iso(1), rating: 8 }, + { ts: iso(1), rating: 7 }, + { ts: iso(1), rating: 9 }, + ]); + + const { count, avg } = synthesize(7).ratings; + + expect(count).toBe(3); + expect(avg).toBe(8); + }); + + test("excludes ratings older than the window", () => { + writeRatings([ + { ts: iso(1), rating: 8 }, + { ts: iso(40), rating: 1 }, + ]); + + expect(synthesize(7).ratings.count).toBe(1); + }); + + test("counts ratings of 3 or below as low", () => { + writeRatings([ + { ts: iso(1), rating: 3 }, + { ts: iso(1), rating: 4 }, + { ts: iso(1), rating: 1 }, + ]); + + expect(synthesize(7).ratings.lowCount).toBe(2); + }); + + test("stays stable when there are too few ratings to compare halves", () => { + writeRatings([ + { ts: iso(1), rating: 1 }, + { ts: iso(1), rating: 10 }, + ]); + + expect(synthesize(7).ratings.trend).toBe("stable"); + }); + + test("reports improving when the later half scores higher", () => { + writeRatings([ + ...Array.from({ length: 4 }, () => ({ ts: iso(2), rating: 4 })), + ...Array.from({ length: 4 }, () => ({ ts: iso(1), rating: 9 })), + ]); + + expect(synthesize(7).ratings.trend).toBe("improving"); + }); + + test("reports declining when the later half scores lower", () => { + writeRatings([ + ...Array.from({ length: 4 }, () => ({ ts: iso(2), rating: 9 })), + ...Array.from({ length: 4 }, () => ({ ts: iso(1), rating: 4 })), + ]); + + expect(synthesize(7).ratings.trend).toBe("declining"); + }); + + test("reports stable when the halves are close", () => { + writeRatings(Array.from({ length: 8 }, () => ({ ts: iso(1), rating: 7 }))); + + expect(synthesize(7).ratings.trend).toBe("stable"); + }); + + test("averages only the last ten ratings for the recent figure", () => { + writeRatings([ + ...Array.from({ length: 10 }, () => ({ ts: iso(2), rating: 2 })), + ...Array.from({ length: 10 }, () => ({ ts: iso(1), rating: 8 })), + ]); + + expect(synthesize(7).ratings.recentAvg).toBe(8); + }); +}); + +describe("synthesize — algorithm reflections", () => { + test("reports zeros with no reflections", () => { + expect(synthesize(7).algorithm).toEqual({ + reflectionCount: 0, + avgSentiment: 0, + passRate: 0, + criteriaTotal: 0, + criteriaPassed: 0, + recentObservations: [], + }); + }); + + test("sums criteria and derives the pass rate as a percentage", () => { + writeReflections([ + reflection({ criteria_count: 4, criteria_passed: 4 }), + reflection({ criteria_count: 6, criteria_passed: 3 }), + ]); + + const { criteriaTotal, criteriaPassed, passRate } = synthesize(7).algorithm; + + expect(criteriaTotal).toBe(10); + expect(criteriaPassed).toBe(7); + expect(passRate).toBe(70); + }); + + test("reports a zero pass rate when no criteria were recorded", () => { + writeReflections([reflection({ criteria_count: 0, criteria_passed: 0 })]); + + expect(synthesize(7).algorithm.passRate).toBe(0); + }); + + test("rounds average sentiment to one decimal", () => { + writeReflections([reflection({ sentiment: 8 }), reflection({ sentiment: 9 })]); + + expect(synthesize(7).algorithm.avgSentiment).toBe(8.5); + }); + + test("keeps only the three most recent observations", () => { + writeReflections([ + reflection({ q1: "first" }), + reflection({ q1: "second" }), + reflection({ q1: "third" }), + reflection({ q1: "fourth" }), + ]); + + const observations = synthesize(7).algorithm.recentObservations; + + expect(observations.map((o) => o.observation)).toEqual(["second", "third", "fourth"]); + }); + + test("carries the task, cwd and date onto each observation", () => { + writeReflections([reflection({ task: "the task", cwd: "/somewhere" })]); + + const [observation] = synthesize(7).algorithm.recentObservations; + + expect(observation.task).toBe("the task"); + expect(observation.cwd).toBe("/somewhere"); + expect(observation.date).toMatch(/^\d{4}-\d{2}-\d{2}$/); + }); + + test("excludes reflections older than the window", () => { + writeReflections([ + reflection({ timestamp: iso(1) }), + reflection({ timestamp: iso(40) }), + ]); + + expect(synthesize(7).algorithm.reflectionCount).toBe(1); + }); +}); + +describe("synthesize — sessions", () => { + test("reports no sessions when the directory is absent", () => { + const state = synthesize(7); + + expect(state.sessions).toEqual([]); + expect(state.sessionCount).toBe(0); + }); + + test("reads the title from a frontmatter title field", () => { + writeSession(ymd(1), 'title: "Fixed the parser"\n\nbody'); + + expect(synthesize(7).sessions[0].titles).toEqual(["Fixed the parser"]); + }); + + test("reads the title from a bold Title marker", () => { + writeSession(ymd(1), "**Title:** Bold style\n\nbody"); + + expect(synthesize(7).sessions[0].titles).toEqual(["Bold style"]); + }); + + test("falls back to the filename when no title is present", () => { + writeSession(ymd(1), "no title here"); + + expect(synthesize(7).sessions[0].titles).toEqual([`${ymd(1)}-run`]); + }); + + test("groups titles by date, newest first", () => { + writeSession(ymd(1), "title: Yesterday"); + writeSession(ymd(0), "title: Today"); + + const dates = synthesize(7).sessions.map((s) => s.date); + + expect(dates).toEqual([...dates].sort().reverse()); + expect(dates[0]).toBe(iso(0).slice(0, 10)); + }); + + test("counts every session across all dates", () => { + writeSession(ymd(1), "title: One"); + writeSession(ymd(2), "title: Two"); + + expect(synthesize(7).sessionCount).toBe(2); + }); + + test("excludes sessions older than the window", () => { + writeSession(ymd(1), "title: Recent"); + writeSession(ymd(40), "title: Ancient"); + + const titles = synthesize(7).sessions.flatMap((s) => s.titles); + + expect(titles).toContain("Recent"); + expect(titles).not.toContain("Ancient"); + }); +}); + +describe("synthesize — envelope", () => { + test("records the requested window and a timestamp", () => { + const state = synthesize(14); + + expect(state.days).toBe(14); + expect(state.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/); + }); +}); + +describe("writeSynthesis", () => { + test("writes the state to synthesis.json and returns its path", () => { + const state = synthesize(7); + + const path = writeSynthesis(state); + + expect(path.endsWith("synthesis.json")).toBe(true); + expect(JSON.parse(readFileSync(path, "utf-8")).days).toBe(7); + }); + + test("writes a signal cache alongside it", () => { + writeRatings([ + { ts: iso(0), rating: 8 }, + { ts: iso(3), rating: 6 }, + ]); + + writeSynthesis(synthesize(7)); + + const cache = JSON.parse( + readFileSync(resolve(HOME, "memory", "state", "signal-cache.json"), "utf-8") + ); + expect(cache.today).toBe(8); + expect(cache.week).toBe(7); + expect(cache.computed_at).toMatch(/^\d{4}-\d{2}-\d{2}T/); + }); + + test("leaves cache windows null when no rating falls in them", () => { + writeRatings([{ ts: iso(60), rating: 9 }]); + + writeSynthesis(synthesize(7)); + + const cache = JSON.parse( + readFileSync(resolve(HOME, "memory", "state", "signal-cache.json"), "utf-8") + ); + expect(cache.today).toBeNull(); + expect(cache.week).toBeNull(); + expect(cache.month).toBeNull(); + }); + + test("marks the cache trend up when recent ratings rise", () => { + writeRatings([ + ...Array.from({ length: 6 }, () => ({ ts: iso(2), rating: 4 })), + ...Array.from({ length: 6 }, () => ({ ts: iso(1), rating: 9 })), + ]); + + writeSynthesis(synthesize(7)); + + const cache = JSON.parse( + readFileSync(resolve(HOME, "memory", "state", "signal-cache.json"), "utf-8") + ); + expect(cache.trend).toBe("up"); + }); + + test("marks the cache trend down when recent ratings fall", () => { + writeRatings([ + ...Array.from({ length: 6 }, () => ({ ts: iso(2), rating: 9 })), + ...Array.from({ length: 6 }, () => ({ ts: iso(1), rating: 4 })), + ]); + + writeSynthesis(synthesize(7)); + + const cache = JSON.parse( + readFileSync(resolve(HOME, "memory", "state", "signal-cache.json"), "utf-8") + ); + expect(cache.trend).toBe("down"); + }); +}); From 8d59446513ef64af1eaa6d81c08a086db2af7c02 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 14:05:33 +0200 Subject: [PATCH 20/35] chore(stryker): raise the mutation threshold to 57 percent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - record rung 3 of the ratchet at a measured 62.22 percent ring score Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- stryker.config.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stryker.config.mjs b/stryker.config.mjs index 13421b8..a6049e0 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -85,7 +85,9 @@ export default { // rung 1 — 2026-08-18: 7627 mutants, 54.87% total / 70.81% covered -> break 50 // rung 2 — 2026-08-18: 7627 mutants, 58.77% total / 70.74% covered -> break 54 // (src/targets/lib.ts 16.45% -> 63.67%, no-coverage 875 -> 183) - thresholds: { high: 80, low: 60, break: 54 }, + // rung 3 — 2026-08-18: 7627 mutants, 62.22% total / 72.44% covered -> break 57 + // (relationship.ts 0.00% -> 75.73%, synthesize.ts 1.79% -> 69.53%) + thresholds: { high: 80, low: 60, break: 57 }, // Stryker copies the project into a sandbox with fs.copyFile, which throws ENOTSUP on a // symlink. Every entry below is either a symlink farm (agent config dirs, the installed // test homes, the vendored skill node_modules) or bulk the suite never reads. From d08fee0e382f9819ab5ef7c8e778f81ceb6c0a59 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 14:17:39 +0200 Subject: [PATCH 21/35] test(hooks): cover stop-handler caching and pending-failure claiming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assert a transcript under two messages and an unparseable one are ignored - assert the last assistant response caches per session, truncated to 2000 chars - assert a supplied last message wins over the one in the transcript - assert a corrupt cache is replaced and other sessions survive an upsert - assert the cache caps at twenty sessions by evicting the oldest - assert a pending failure is claimed out of the state directory - ignore the per-suite sandboxes a detached child can recreate after cleanup Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .gitignore | 2 + test/stop-handlers.test.ts | 175 +++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 test/stop-handlers.test.ts diff --git a/.gitignore b/.gitignore index 5fa55e0..9125205 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ node_modules/ .test-tmp/ .test-home/ +# Per-suite sandboxes; a detached child can recreate one after its test cleans up +.test-home-*/ # Eval runner logs (promptfoo writes error/debug logs to cwd) eval/logs/ diff --git a/test/stop-handlers.test.ts b/test/stop-handlers.test.ts new file mode 100644 index 0000000..d4ed8c9 --- /dev/null +++ b/test/stop-handlers.test.ts @@ -0,0 +1,175 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { runStopHandlers } from "../src/hooks/lib/stop"; + +// runStopHandlers spawns detached children that keep writing into PAL_HOME after +// the test returns, so this directory can reappear after cleanup — .gitignore +// covers .test-home-* for exactly that reason. +const HOME = resolve(import.meta.dir, "../.test-home-stop-handlers"); +const savedHome = process.env.PAL_HOME; + +function transcriptOf(...contents: string[]): string { + return JSON.stringify( + contents.map((content, i) => ({ + role: i % 2 === 0 ? "user" : "assistant", + content, + })) + ); +} + +function cachePath(): string { + return resolve(HOME, "memory", "state", "last-responses.json"); +} + +function readCache(): Record<string, { response: string; ts: string }> { + return JSON.parse(readFileSync(cachePath(), "utf-8")); +} + +function seedCache(entries: Record<string, { response: string; ts: string }>) { + mkdirSync(resolve(HOME, "memory", "state"), { recursive: true }); + writeFileSync(cachePath(), JSON.stringify(entries), "utf-8"); +} + +beforeEach(() => { + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); + process.env.PAL_HOME = HOME; +}); + +afterEach(() => { + if (savedHome === undefined) delete process.env.PAL_HOME; + else process.env.PAL_HOME = savedHome; + rmSync(HOME, { recursive: true, force: true }); +}); + +describe("runStopHandlers — transcript gate", () => { + test("does nothing for a transcript with fewer than two messages", async () => { + await runStopHandlers(transcriptOf("only one"), { sessionId: "s1" }); + + expect(existsSync(cachePath())).toBe(false); + }); + + test("does nothing for an unparseable transcript", async () => { + await runStopHandlers("not json at all", { sessionId: "s1" }); + + expect(existsSync(cachePath())).toBe(false); + }); +}); + +describe("runStopHandlers — last-response cache", () => { + test("caches the last assistant message under the session id", async () => { + await runStopHandlers(transcriptOf("q", "the answer"), { sessionId: "sess-a" }); + + expect(readCache()["sess-a"].response).toBe("the answer"); + expect(readCache()["sess-a"].ts).toMatch(/^\d{4}-\d{2}-\d{2}T/); + }); + + test("prefers an explicitly supplied last assistant message", async () => { + await runStopHandlers(transcriptOf("q", "from transcript"), { + sessionId: "sess-a", + lastAssistantMessage: "supplied directly", + }); + + expect(readCache()["sess-a"].response).toBe("supplied directly"); + }); + + test("writes no cache when there is no session id", async () => { + await runStopHandlers(transcriptOf("q", "an answer")); + + expect(existsSync(cachePath())).toBe(false); + }); + + test("truncates a long response to 2000 characters", async () => { + await runStopHandlers(transcriptOf("q", "x".repeat(5000)), { sessionId: "sess-a" }); + + expect(readCache()["sess-a"].response).toHaveLength(2000); + }); + + test("overwrites the entry for the same session", async () => { + await runStopHandlers(transcriptOf("q", "first"), { sessionId: "sess-a" }); + await runStopHandlers(transcriptOf("q", "second"), { sessionId: "sess-a" }); + + const cache = readCache(); + expect(Object.keys(cache)).toEqual(["sess-a"]); + expect(cache["sess-a"].response).toBe("second"); + }); + + test("keeps entries belonging to other sessions", async () => { + await runStopHandlers(transcriptOf("q", "one"), { sessionId: "sess-a" }); + await runStopHandlers(transcriptOf("q", "two"), { sessionId: "sess-b" }); + + expect(Object.keys(readCache()).sort()).toEqual(["sess-a", "sess-b"]); + }); + + test("starts a fresh cache when the existing file is corrupt", async () => { + mkdirSync(resolve(HOME, "memory", "state"), { recursive: true }); + writeFileSync(cachePath(), "{ not json", "utf-8"); + + await runStopHandlers(transcriptOf("q", "recovered"), { sessionId: "sess-a" }); + + expect(readCache()["sess-a"].response).toBe("recovered"); + }); + + test("caps the cache at twenty sessions, evicting the oldest", async () => { + const seeded: Record<string, { response: string; ts: string }> = {}; + for (let i = 0; i < 20; i++) { + seeded[`old-${String(i).padStart(2, "0")}`] = { + response: "r", + ts: `2020-01-${String(i + 1).padStart(2, "0")}T00:00:00.000Z`, + }; + } + seedCache(seeded); + + await runStopHandlers(transcriptOf("q", "newest"), { sessionId: "fresh" }); + + const cache = readCache(); + expect(Object.keys(cache)).toHaveLength(20); + expect(cache.fresh).toBeDefined(); + expect(cache["old-00"]).toBeUndefined(); + expect(cache["old-19"]).toBeDefined(); + }); + + test("leaves the cache untouched at exactly twenty sessions", async () => { + const seeded: Record<string, { response: string; ts: string }> = {}; + for (let i = 0; i < 19; i++) { + seeded[`keep-${String(i).padStart(2, "0")}`] = { + response: "r", + ts: `2020-01-${String(i + 1).padStart(2, "0")}T00:00:00.000Z`, + }; + } + seedCache(seeded); + + await runStopHandlers(transcriptOf("q", "twentieth"), { sessionId: "fresh" }); + + const cache = readCache(); + expect(Object.keys(cache)).toHaveLength(20); + expect(cache["keep-00"]).toBeDefined(); + }); +}); + +describe("runStopHandlers — pending failure claim", () => { + function pendingPath(): string { + return resolve(HOME, "memory", "state", "pending-failure.json"); + } + + test("claims a pending failure by moving it out of the state directory", async () => { + mkdirSync(resolve(HOME, "memory", "state"), { recursive: true }); + writeFileSync( + pendingPath(), + JSON.stringify({ rating: 3, context: "bad", cwd: HOME }), + "utf-8" + ); + + await runStopHandlers(transcriptOf("q", "an answer"), { sessionId: "sess-a" }); + + expect(existsSync(pendingPath())).toBe(false); + }); + + test("completes normally when no pending failure exists", async () => { + await runStopHandlers(transcriptOf("q", "an answer"), { sessionId: "sess-a" }); + + expect(existsSync(pendingPath())).toBe(false); + expect(readCache()["sess-a"]).toBeDefined(); + }); +}); From a1f7e8987cebe61d0ba685811d81f399b445b082 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 14:32:22 +0200 Subject: [PATCH 22/35] test(hooks): cover context assembly with content assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assert wisdom principles render with domain and confidence above the bar - assert the learning digest lists cross-project titles and caps at five - assert relationship notes keep world facts and scope sessions to the project - assert opinion entries and html comments are stripped - assert session intelligence renders trend, pass-rate, and project observations - assert the handoff surfaces while in progress and lapses after a week - assert semi-static content injects only when no agent loads it natively Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/context-build.test.ts | 438 +++++++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 test/context-build.test.ts diff --git a/test/context-build.test.ts b/test/context-build.test.ts new file mode 100644 index 0000000..40554dd --- /dev/null +++ b/test/context-build.test.ts @@ -0,0 +1,438 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { + buildSystemReminder, + loadLearningDigest, + loadRelationshipContext, + loadWisdomContext, +} from "../src/hooks/lib/context"; + +const HOME = resolve(import.meta.dir, "../.test-home-context-build"); +const savedHome = process.env.PAL_HOME; + +function write(relPath: string, content: string) { + const full = resolve(HOME, relPath); + mkdirSync(resolve(full, ".."), { recursive: true }); + writeFileSync(full, content, "utf-8"); +} + +function frame(domain: string, body: string) { + write(`memory/wisdom/frames/${domain}.md`, body); +} + +function today(offset = 0): { month: string; day: string } { + const d = new Date(); + d.setDate(d.getDate() - offset); + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return { month: `${yyyy}-${mm}`, day: `${yyyy}-${mm}-${dd}` }; +} + +function notes(body: string) { + const { month, day } = today(); + write(`memory/relationship/${month}/${day}.md`, body); +} + +function learning(title: string, cwd: string, offset = 0) { + const d = new Date(); + d.setDate(d.getDate() - offset); + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const stamp = `${yyyy}${mm}${String(d.getDate()).padStart(2, "0")}-${title.replace(/\W/g, "")}`; + write( + `memory/learning/session/${yyyy}/${mm}/${stamp}.md`, + `---\ntitle: "${title}"\ncwd: ${cwd}\n---\n\nbody\n` + ); +} + +beforeEach(() => { + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); + process.env.PAL_HOME = HOME; +}); + +afterEach(() => { + if (savedHome === undefined) delete process.env.PAL_HOME; + else process.env.PAL_HOME = savedHome; + rmSync(HOME, { recursive: true, force: true }); +}); + +describe("loadWisdomContext", () => { + test("is empty when no frames exist", () => { + expect(loadWisdomContext()).toBe(""); + }); + + test("lists a crystallized principle under a heading", () => { + frame("development", "### Always measure first [CRYSTAL: 90%]\nbody"); + + const out = loadWisdomContext(); + + expect(out).toContain("## Crystallized Principles"); + expect(out).toContain("- [development] Always measure first (90%)"); + }); + + test("omits a principle below the confidence bar", () => { + frame("development", "### Too uncertain [CRYSTAL: 60%]\nbody"); + + expect(loadWisdomContext()).toBe(""); + }); + + test("keeps a principle exactly at the bar", () => { + frame("development", "### Right at the line [CRYSTAL: 85%]\nbody"); + + expect(loadWisdomContext()).toContain("Right at the line"); + }); +}); + +describe("loadLearningDigest", () => { + test("is empty when nothing has been learned", () => { + expect(loadLearningDigest()).toBe(""); + }); + + test("omits learnings from the current project", () => { + learning("Same project thing", process.cwd()); + + expect(loadLearningDigest()).toBe(""); + }); + + test("lists learnings from other projects under a heading", () => { + learning("Elsewhere thing", "/some/other/project"); + + const out = loadLearningDigest(); + + expect(out).toContain("## Other Recent Learnings"); + expect(out).toContain("- Elsewhere thing"); + }); + + test("lists at most five cross-project learnings", () => { + for (let i = 0; i < 8; i++) learning(`Thing ${i}`, `/other/${i}`, i); + + const listed = loadLearningDigest() + .split("\n") + .filter((l) => l.startsWith("- ")); + + expect(listed).toHaveLength(5); + }); +}); + +describe("loadRelationshipContext", () => { + test("is empty when there are no notes", () => { + expect(loadRelationshipContext()).toBe(""); + }); + + test("keeps world facts under a heading", () => { + notes("## 09:00\n- W: uses bun everywhere\n"); + + const out = loadRelationshipContext(); + + expect(out).toContain("## Recent Interaction Notes"); + expect(out).toContain("- W: uses bun everywhere"); + }); + + test("strips opinion entries, which load natively elsewhere", () => { + notes("## 09:00\n- O(c=0.9): prefers terse replies\n- W: a fact\n"); + + const out = loadRelationshipContext(); + + expect(out).not.toContain("prefers terse replies"); + expect(out).toContain("- W: a fact"); + }); + + test("keeps a session entry recorded in the current project", () => { + notes( + `## 09:00\n<!-- session:abc cwd:${process.cwd()} -->\n- Session: did the thing\n` + ); + + expect(loadRelationshipContext()).toContain("- Session: did the thing"); + }); + + test("drops a session entry recorded in another project", () => { + notes("## 09:00\n<!-- session:abc cwd:/elsewhere -->\n- Session: unrelated work\n"); + + expect(loadRelationshipContext()).not.toContain("unrelated work"); + }); + + test("keeps a legacy session entry that carries no cwd", () => { + notes("## 09:00\n- Session: legacy entry\n"); + + expect(loadRelationshipContext()).toContain("- Session: legacy entry"); + }); + + test("strips the html comments themselves", () => { + notes("## 09:00\n<!-- session:abc cwd:/elsewhere -->\n- W: a fact\n"); + + expect(loadRelationshipContext()).not.toContain("<!--"); + }); + + test("resets project scoping at each timestamp block", () => { + notes( + `## 09:00\n<!-- session:a cwd:/elsewhere -->\n- Session: other project\n` + + `## 10:00\n- Session: no cwd so kept\n` + ); + + const out = loadRelationshipContext(); + + expect(out).not.toContain("other project"); + expect(out).toContain("no cwd so kept"); + }); +}); + +describe("buildSystemReminder", () => { + // A bare home is not silent: the analyze nudge and the unregistered-project + // hint both fire, which is the behaviour worth pinning here. + test("surfaces the analyze nudge when analysis has never run", () => { + const out = buildSystemReminder(); + + expect(out).toContain("## Learning Analysis Due"); + expect(out).toContain("/pal-analyze"); + }); + + test("wraps content in a system-reminder with the current time", () => { + notes("## 09:00\n- W: a fact\n"); + + const out = buildSystemReminder(); + + expect(out.startsWith("<system-reminder>")).toBe(true); + expect(out.trimEnd().endsWith("</system-reminder>")).toBe(true); + expect(out).toContain("**Current time:**"); + }); + + test("omits wisdom for an agent that loads it natively", () => { + frame("development", "### Native principle [CRYSTAL: 90%]\nbody"); + notes("## 09:00\n- W: a fact\n"); + + expect(buildSystemReminder({ agent: "claude" })).not.toContain("Native principle"); + }); + + // Every member of AgentTarget loads semi-static context natively, so the hook + // only injects it when no agent is named — the path Codex takes. + test("includes wisdom when no agent is named", () => { + frame("development", "### Injected principle [CRYSTAL: 90%]\nbody"); + + expect(buildSystemReminder()).toContain("Injected principle"); + }); + + test("omits wisdom for every agent that loads it natively", () => { + frame("development", "### Native principle [CRYSTAL: 90%]\nbody"); + + for (const agent of ["claude", "opencode", "cursor", "copilot"] as const) { + expect(buildSystemReminder({ agent })).not.toContain("Native principle"); + } + }); + + test("still includes relationship notes for a native-loading agent", () => { + notes("## 09:00\n- W: still injected\n"); + + expect(buildSystemReminder({ agent: "claude" })).toContain("- W: still injected"); + }); +}); + +function synthesis(state: Record<string, unknown>) { + write("memory/state/synthesis.json", JSON.stringify(state)); +} + +function ratings(over: Record<string, unknown> = {}) { + return { count: 10, avg: 7, recentAvg: 7, lowCount: 0, trend: "stable", ...over }; +} + +function handoff(over: Record<string, unknown> = {}) { + write( + "memory/state/last-handoff.json", + JSON.stringify({ + [process.cwd()]: { + handoff: "the remaining work", + title: "a previous session", + status: "in-progress", + timestamp: new Date().toISOString(), + ...over, + }, + }) + ); +} + +describe("session intelligence", () => { + test("is absent when no synthesis has been written", () => { + expect(buildSystemReminder()).not.toContain("## Session Intelligence"); + }); + + test("reports the rating trend line", () => { + synthesis({ ratings: ratings({ avg: 8, recentAvg: 9, trend: "improving" }) }); + + const out = buildSystemReminder(); + + expect(out).toContain("**Rating trend:** 8/10 avg (last 10: 9/10, improving)."); + expect(out).toContain("→ Trend is improving. Maintain current approach."); + }); + + test("warns when the trend is declining", () => { + synthesis({ ratings: ratings({ trend: "declining" }) }); + + expect(buildSystemReminder()).toContain("→ Trend is declining."); + }); + + test("notes the low-rating count when there is one", () => { + synthesis({ ratings: ratings({ lowCount: 2 }) }); + + expect(buildSystemReminder()).toContain("2 low ratings."); + }); + + test("omits the low-rating note when there are none", () => { + synthesis({ ratings: ratings({ lowCount: 0 }) }); + + expect(buildSystemReminder()).not.toContain("low ratings."); + }); + + test("advises slowing down when many ratings are low and the trend is flat", () => { + synthesis({ ratings: ratings({ lowCount: 6, trend: "stable" }) }); + + expect(buildSystemReminder()).toContain("→ Multiple low ratings."); + }); + + test("skips the ratings block when nothing was rated", () => { + synthesis({ ratings: ratings({ count: 0 }) }); + + expect(buildSystemReminder()).not.toContain("**Rating trend:**"); + }); + + test("reports algorithm performance", () => { + synthesis({ + algorithm: { + reflectionCount: 4, + passRate: 95, + avgSentiment: 8, + recentObservations: [], + }, + }); + + expect(buildSystemReminder()).toContain( + "**Algorithm:** 4 reflections, 95% criteria pass rate, 8/10 sentiment." + ); + }); + + test("flags a low criteria pass rate", () => { + synthesis({ + algorithm: { + reflectionCount: 4, + passRate: 60, + avgSentiment: 8, + recentObservations: [], + }, + }); + + expect(buildSystemReminder()).toContain("→ Criteria pass rate is low."); + }); + + test("stays quiet about a healthy pass rate", () => { + synthesis({ + algorithm: { + reflectionCount: 4, + passRate: 95, + avgSentiment: 8, + recentObservations: [], + }, + }); + + expect(buildSystemReminder()).not.toContain("Criteria pass rate is low"); + }); + + test("shows observations recorded in this project", () => { + synthesis({ + algorithm: { + reflectionCount: 1, + passRate: 90, + avgSentiment: 8, + recentObservations: [ + { + date: "2026-08-18", + cwd: process.cwd(), + task: "a task", + observation: "a lesson", + }, + ], + }, + }); + + const out = buildSystemReminder(); + + expect(out).toContain("Recent self-observations (this project):"); + expect(out).toContain('- [2026-08-18] a task: "a lesson"'); + }); + + test("hides observations recorded elsewhere", () => { + synthesis({ + algorithm: { + reflectionCount: 1, + passRate: 90, + avgSentiment: 8, + recentObservations: [ + { + date: "2026-08-18", + cwd: "/elsewhere", + task: "other", + observation: "not mine", + }, + ], + }, + }); + + expect(buildSystemReminder()).not.toContain("not mine"); + }); + + test("ignores a malformed synthesis file", () => { + write("memory/state/synthesis.json", "{ not json"); + + expect(buildSystemReminder()).not.toContain("## Session Intelligence"); + }); +}); + +describe("handoff", () => { + test("is absent when no handoff was recorded", () => { + expect(buildSystemReminder()).not.toContain("Pick Up Where You Left Off"); + }); + + test("surfaces an in-progress handoff for this project", () => { + handoff(); + + const out = buildSystemReminder(); + + expect(out).toContain("## Pick Up Where You Left Off"); + expect(out).toContain("*Previous session: a previous session*"); + expect(out).toContain("the remaining work"); + }); + + test("stays silent once the handoff is done", () => { + handoff({ status: "done" }); + + expect(buildSystemReminder()).not.toContain("Pick Up Where You Left Off"); + }); + + test("drops a handoff older than a week", () => { + handoff({ timestamp: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString() }); + + expect(buildSystemReminder()).not.toContain("Pick Up Where You Left Off"); + }); + + test("keeps a handoff from within the week", () => { + handoff({ timestamp: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString() }); + + expect(buildSystemReminder()).toContain("Pick Up Where You Left Off"); + }); + + test("ignores a handoff belonging to another project", () => { + write( + "memory/state/last-handoff.json", + JSON.stringify({ + "/elsewhere": { + handoff: "someone else work", + title: "t", + status: "in-progress", + timestamp: new Date().toISOString(), + }, + }) + ); + + expect(buildSystemReminder()).not.toContain("someone else work"); + }); +}); From 513f2b3c851d09438be3932736a77dcf2338aa3b Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 14:56:54 +0200 Subject: [PATCH 23/35] chore(stryker): scope the ratchet to the diff gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - raise the threshold from the modules a change touches, not a whole-ring run - record why the whole-ring run costs 45 minutes for a three point move Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- stryker.config.mjs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/stryker.config.mjs b/stryker.config.mjs index a6049e0..66211ec 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -79,14 +79,20 @@ export default { inspectorTimeout: 60000, }, reporters: ["clear-text", "progress", "html"], - // Ratchet. `break` sits ~5 points below the last measured run so a normal change - // has headroom. Raise it only after a run beats the current number; lower it only - // with the reason written here. - // rung 1 — 2026-08-18: 7627 mutants, 54.87% total / 70.81% covered -> break 50 - // rung 2 — 2026-08-18: 7627 mutants, 58.77% total / 70.74% covered -> break 54 + // Ratchet. `break` applies to whatever a run mutates, and the only run that gates + // anything is the diff one — CI never runs the whole ring. Raise it after the + // modules a change touches measure above the new number + // (`bunx stryker run --mutate <file>`); lower it only with the reason written here. + // + // Do not re-baseline by running the whole ring: 766 of its mutants are static + // (module-load code Stryker cannot map perTest coverage onto), so it re-runs the + // full suite for each and takes ~45 minutes to move this number by ~3 points. + // rung 1 — 2026-08-18: 54.87% ring -> break 50 + // rung 2 — 2026-08-18: 58.77% ring -> break 54 // (src/targets/lib.ts 16.45% -> 63.67%, no-coverage 875 -> 183) - // rung 3 — 2026-08-18: 7627 mutants, 62.22% total / 72.44% covered -> break 57 + // rung 3 — 2026-08-18: 62.22% ring -> break 57 [last whole-ring measurement] // (relationship.ts 0.00% -> 75.73%, synthesize.ts 1.79% -> 69.53%) + // since: context.ts 2.70% -> 63.24%, stop.ts 1.82% -> 77.27% thresholds: { high: 80, low: 60, break: 57 }, // Stryker copies the project into a sandbox with fs.copyFile, which throws ENOTSUP on a // symlink. Every entry below is either a symlink farm (agent config dirs, the installed From 45445355490d3cad602e22047b34a7fe741d395b Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 15:12:19 +0200 Subject: [PATCH 24/35] fix(test): locate the quarantined copy with a portable directory walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replace the sh find pipeline with readdirSync so the check runs on Windows - sort the hits so the asserted match is deterministic Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/import-merge.test.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/test/import-merge.test.ts b/test/import-merge.test.ts index 4490fc6..adf3efd 100644 --- a/test/import-merge.test.ts +++ b/test/import-merge.test.ts @@ -4,6 +4,7 @@ import { existsSync, mkdirSync, mkdtempSync, + readdirSync, readFileSync, rmSync, writeFileSync, @@ -13,6 +14,18 @@ import { resolve } from "node:path"; const CLI = resolve(import.meta.dir, "../src/cli/index.ts"); +/** Every path named `name` under `dir`, sorted so the first hit is stable. */ +function findUnder(dir: string, name: string): string[] { + if (!existsSync(dir)) return []; + const hits: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true, recursive: true })) { + if (entry.isFile() && entry.name === name) { + hits.push(resolve(entry.parentPath, entry.name)); + } + } + return hits.sort(); +} + let SRC: string; let DST: string; let WORK: string; @@ -111,15 +124,9 @@ describe("import into a NON-EMPTY home", () => { // local wins in place expect(read(DST, "telos/GOALS.md")).toContain("linux version"); // incoming is preserved somewhere under backups/, not silently dropped - const found = spawnSync( - "sh", - ["-c", `find ${DST}/backups -name GOALS.md | head -1`], - { - encoding: "utf-8", - } - ).stdout.trim(); + const found = findUnder(resolve(DST, "backups"), "GOALS.md"); expect(found.length).toBeGreaterThan(0); - expect(readFileSync(found, "utf-8")).toContain("mac version"); + expect(readFileSync(found[0], "utf-8")).toContain("mac version"); }); test("is idempotent — importing twice does not duplicate records", () => { From c08f347e88103abb832490461ae1b5d097767582 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 15:25:49 +0200 Subject: [PATCH 25/35] chore(klint): upgrade to 0.34.0 and install the skill as copies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - give each agent directory its own klint-rules skill instead of a link into node_modules - clear the klint/skill-legacy-link warnings the new version reports Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .agents/skills/klint-rules | 1 - .agents/skills/klint-rules/.klint-skill.json | 4 + .agents/skills/klint-rules/SKILL.md | 252 +++++++++++++++++++ .claude/skills/klint-rules | 1 - .claude/skills/klint-rules/.klint-skill.json | 4 + .claude/skills/klint-rules/SKILL.md | 252 +++++++++++++++++++ .cursor/skills/klint-rules | 1 - .cursor/skills/klint-rules/.klint-skill.json | 4 + .cursor/skills/klint-rules/SKILL.md | 252 +++++++++++++++++++ bun.lock | 12 +- package.json | 2 +- 11 files changed, 775 insertions(+), 10 deletions(-) delete mode 120000 .agents/skills/klint-rules create mode 100644 .agents/skills/klint-rules/.klint-skill.json create mode 100644 .agents/skills/klint-rules/SKILL.md delete mode 120000 .claude/skills/klint-rules create mode 100644 .claude/skills/klint-rules/.klint-skill.json create mode 100644 .claude/skills/klint-rules/SKILL.md delete mode 120000 .cursor/skills/klint-rules create mode 100644 .cursor/skills/klint-rules/.klint-skill.json create mode 100644 .cursor/skills/klint-rules/SKILL.md diff --git a/.agents/skills/klint-rules b/.agents/skills/klint-rules deleted file mode 120000 index b250658..0000000 --- a/.agents/skills/klint-rules +++ /dev/null @@ -1 +0,0 @@ -../../node_modules/@konvert7/klint/skill/klint-rules \ No newline at end of file diff --git a/.agents/skills/klint-rules/.klint-skill.json b/.agents/skills/klint-rules/.klint-skill.json new file mode 100644 index 0000000..f267932 --- /dev/null +++ b/.agents/skills/klint-rules/.klint-skill.json @@ -0,0 +1,4 @@ +{ + "version": "0.34.0", + "sha256": "c8c9584cee2ef21246fc0d0498e39d7a1af7b965aff875bbb8f55c608004357b" +} diff --git a/.agents/skills/klint-rules/SKILL.md b/.agents/skills/klint-rules/SKILL.md new file mode 100644 index 0000000..0226fef --- /dev/null +++ b/.agents/skills/klint-rules/SKILL.md @@ -0,0 +1,252 @@ +--- +name: klint-rules +description: Add, modify, or explain klint architecture rules for this repo. Use when asked to enforce a new structural constraint, adjust rule scope, or explain why a violation fired. +argument-hint: <constraint to enforce> +--- + +## Schema + +The full, authoritative config schema lives in the repo and is the source of truth for every field below: + +- YAML: <https://github.com/konvert7/klint/blob/main/klint.schema.yaml> +- JSON: <https://github.com/konvert7/klint/blob/main/klint.schema.json> + +Wire it into `klint.yaml` for editor autocomplete and validation: + +```yaml +# yaml-language-server: $schema=./klint.schema.yaml +``` + +## Workflow + +1. **Understand the constraint** — if scope is ambiguous, ask: which files/layers are involved? should it block or warn? are there legitimate exceptions? + +2. **Grep first** — verify the pattern exists and count current occurrences before touching `klint.yaml`: + ```sh + grep -rn "the-pattern" src/ --include="*.ts" + ``` + +3. **Choose the right primitive:** + + | The constraint is... | Primitive | + |---|---| + | Layer X must not import from layer Y | `arch.imports` + `deny` | + | Layer X may only import from Y and Z | `arch.imports` + `allow` | + | Layer X must not import an npm package or `node:` builtin | `arch.imports` + `deny-packages` | + | Pattern P must only appear in one designated file | `arch.singleton` + `pattern` | + | Pattern P must never appear inside a scoped layer | `arch.forbidden` + `pattern` | + | Raw HTML/JSX element must never appear in a scoped layer | `arch.forbidden` + `jsx-element` | + | Raw HTML/JSX element may only appear in one primitive file | `arch.singleton` + `jsx-element` | + | A value-shaped pattern (regex) must never appear in a scoped layer | `arch.forbidden` + `pattern: "re:…"` | + | No file in a layer may exceed N lines | `arch.maxLines` + `limit` | + | No file may be more than N% comments | `arch.maxCommentDensity` + `limit` | + | No comment block may stack more than N lines | `arch.maxCommentBlock` + `limit` | + +4. **Read `klint.yaml`** — check existing `arch.layers` and rules before adding anything. Add a new named layer to `arch.layers` if the file group doesn't exist yet. + +5. **Write the stanza** in `klint.yaml` under the correct `arch:` section. + +6. **Verify zero new violations on the current codebase:** + ```sh + bun klint/cli.ts # in-repo + npx klint # after npm install + ``` + If new violations appear on existing code, fix them first or adjust the rule scope — then land the rule. + +7. **Write a break test** — prove the rule fires on a deliberate violation, and doesn't fire on clean code. Run the test suite to confirm. + +## Primitive reference + +### Layers +```yaml +arch: + layers: + core: ["src/lib/**", "src/hooks/**"] + ui: ["src/components/**"] + targets: ["src/targets/**"] +``` + +### Import boundaries +```yaml +arch: + imports: + # deny: block imports from one layer into another + - from: core + deny: targets + message: "Core must not depend on agent-specific code" + severity: error # optional — default is error; use warn to record without blocking + + # allow: whitelist — anything not listed is denied (npm + node: builtins always pass) + - from: ["src/dao/**"] + allow: ["src/dao/**", "src/prisma/**"] + message: "DAO may only import from dao or prisma" + + # type-only: allow — import type {} and Python if TYPE_CHECKING: blocks stay permitted + - from: core + deny: targets + type-only: allow + message: "Core must not depend on agent-specific code" + + # deny-packages: block npm packages and node: builtins, which deny/allow cannot reach + # because they never resolve to a project file. Composes with deny in the same rule. + - from: ["src/dao/**"] + deny-packages: ["next/headers", "node:fs"] + message: "Data access must not read request state or touch the filesystem" +``` + +`deny-packages` matches per segment: `next` covers `next/headers`, `nextra` is unaffected, and +`next/headers` does not match `next/navigation`. Python uses dotted segments, so `os` covers +`os.path` and blocks pip packages and stdlib modules alike. Swift uses dotted segments too, at +module granularity, which is what makes system frameworks like `UIKit` reachable. Rust splits on +`::`, so `tokio` covers `tokio::sync::Mutex` while `tokio_util` is unaffected. The specifier is read off the AST, so +static imports, dynamic `import()`, and `export … from` re-exports all count while a comment +mentioning the package does not — unlike `forbidden` + `pattern`, which is a line-based text scan. + +### Singleton — one designated location + +Pin a `pattern` (literal string) **or** a `jsx-element` (AST-matched tag) to exactly one file. Every other in-scope occurrence is a violation. + +```yaml +arch: + singleton: + # pattern: literal string match — fires if it appears anywhere except `only` + - pattern: "process.env.API_KEY" + only: "src/lib/auth.ts" + in: ["src/**"] # optional: limit scan scope (default: all files). string OR array + message: "Use the auth module instead of reading API_KEY directly" + severity: error # optional — default error; use warn to record without blocking + + # jsx-element: pin a raw element to its design-system primitive + - jsx-element: "button" # one tag, or a list: ["button", "input"] + only: "src/components/ui/button.tsx" + in: ["src/**/*.tsx"] + message: "Raw <button> belongs only to the Button primitive" +``` + +Required: `only` + `message`, plus one of `pattern` / `jsx-element` (not both in the same stanza). + +### Forbidden — banned pattern in scope + +Block a `pattern` (literal string) **or** a `jsx-element` (AST-matched tag) inside a scoped layer. + +```yaml +arch: + forbidden: + # pattern: literal substring by default; prefix with `re:` for a regex + - pattern: "console.log(" + in: ["src/lib/**"] # string OR array; supports ! negation + message: "Use the logger — console.log leaks into the agent event stream" + severity: error # optional — default error; use warn to record without blocking + + # re: prefix — match a regular expression (common JS/RE2 subset; no lookaround/backrefs) + - pattern: 're:\b(?:p|gap)-\[' + in: ["src/**/*.tsx"] + message: "Use the spacing scale, not arbitrary bracket values like p-[18px]" + + # jsx-element: forbid raw HTML elements outside the design system + - jsx-element: ["button", "input", "label"] + in: ["src/app/**/*.tsx", "src/components/**/*.tsx", "!src/components/ui/**"] + message: "Use the design-system primitives in @/components/ui/* instead of raw HTML elements" +``` + +Required: `in` + `message`, plus one of `pattern` / `jsx-element` (not both in the same stanza). A `pattern` may be a literal substring or, with the `re:` prefix, a regex; `singleton` patterns accept `re:` too. + +`jsx-element` matches intrinsic element names on the AST (opening and self-closing tags), so it is robust to whitespace, attributes, and naming collisions like `<buttonGroup>` — unlike a literal `pattern: "<button"` scan. It works on `.tsx`/`.jsx` files in both the TS and Rust engines. + +### Max lines — cap file length + +Limit how many physical lines a file in scope may have. Use a separate stanza per scope to give different ceilings to source and tests. + +```yaml +arch: + maxLines: + - limit: 300 # positive integer; required + in: ["src/**"] # string OR array; supports ! negation + message: "Split this module" # optional — defaults to "File exceeds the maximum of N lines" + severity: error # optional — default error; use warn to record without blocking + + - limit: 600 # tests may run longer + in: ["tests/**"] +``` + +Required: `limit` + `in`. The count is **total physical lines** — blanks and comments included, not code lines — and a file over the limit is flagged at line `limit + 1`. Enforced identically in the TS and Rust engines, across every language klint scans. + +### Comment budget — cap explanatory prose + +Two ceilings on how much of a file may be comments. Use them to push intent into names and small +functions rather than paragraphs that drift from the code they describe. + +```yaml +arch: + # maxCommentDensity: what share of a file may be comment lines + maxCommentDensity: + - limit: 5 # percent; required + in: ["src/**"] # string OR array; supports ! negation + countDocComments: false # optional — default false, so JSDoc/docstrings are exempt + message: "Encode intent in names and small functions, not prose" + severity: error # optional — default error; use warn to record without blocking + + # maxCommentBlock: how tall a single stack of comment lines may get + maxCommentBlock: + - limit: 2 # consecutive comment lines; required + in: ["src/**"] + ignore: ["@codegen:"] # optional — structural markers, not prose + message: "Extract a well-named function instead of stacking comment lines" +``` + +Required: `limit` + `in`. Density is a **percentage of total physical lines** (code + comments + +blanks — the same denominator `maxLines` uses). Block height counts **consecutive** comment lines and +is reported at the first offending line. Both exempt doc-comments by default — `/** … */` JSDoc and +Python docstrings — so documentation is not penalised; ordinary `//`, `/* */`, and `#` comments always +count. Set `countDocComments: true` to include them. + +The two are complementary: density catches a file that is 30% commented in scattered one-liners, +block catches a ten-line essay above one function in an otherwise sparse file. Neither alone catches both. + +**`ignore` — comments that are machinery, not prose.** Some comments are load-bearing input to a +build or codegen step (`// @codegen:region-start`, feature-strip markers, pragmas). They are comments to the +lexer but structure to the project, and counting them makes both limits measure the wrong thing: + +```yaml +arch: + maxCommentDensity: + - limit: 5 + in: ["src/**"] + ignore: ["@codegen:"] # literal substring, or "re:" for a regex +``` + +Two rules govern how an ignored line behaves, and both matter: + +- **It stays in the density denominator.** A marker is a physical line like code, so a 20-line file + with 4 markers and 1 real comment measures 1/20, not 1/16. +- **It does not break a comment block.** `// a` / `// @marker` / `// b` / `// c` is a run of three + counted lines, not two runs. Otherwise sprinkling a marker every other line would defeat the cap. + +`ignore` tests the **physical source line**, so it only ever applies to lines already identified as +comments — code containing the same text is untouched. Enforced identically in both engines. + +## Pitfalls + +- `pattern` in `singleton` and `forbidden` is a **literal substring** by default — it will not catch `process.env["KEY"]` (bracket notation). Either grep for both forms, or use a `re:` regex prefix to match them in one rule. Caveat: a literal pattern that itself begins with `re:` cannot be expressed (it is always read as a regex), and regexes must stay in the common JS/RE2 subset (no lookaround or backreferences) so both engines agree. +- `maxLines.limit` counts **total physical lines** (blanks and comments included), not code lines; a trailing newline does not add a line, and both engines count identically. +- `maxCommentDensity.limit` is a **percentage, not a line count** — `limit: 5` means 5% of the file, so a 40-line file is over budget at its third comment line. Measure before picking a number: land it as `severity: warn` first, read the reported densities, then tighten to `error`. +- `jsx-element` matches **intrinsic** (lowercase HTML) tags only — `button`, `input`, `label`. It does not match custom React components like `<Button>`; to restrict those, use `arch.imports` against the component's module instead. +- A `forbidden`/`singleton` stanza takes either `pattern` or `jsx-element`, never both — split into two stanzas if you need both. +- `only` in `singleton` is relative to the project root — use forward slashes on all platforms. +- `severity: warn` records the violation but does not block the session. Use `error` (the default) to block. +- Path aliases (`@/*`) resolve via `tsconfig.json` `compilerOptions.paths`, including `extends` chains. +- Adding a rule that fires on existing code is not a blocker — fix the violations first, then add the rule. + +## Output format + +After adding or modifying a rule, report: +- The YAML stanza added +- The grep evidence that confirmed the pattern is real +- The test written and its result +- The output of `bun run klint` (or `npx klint`) confirming zero new violations + +## Do NOT use + +- To fix TypeScript type errors, lint issues, or Biome violations — those are separate tools +- To enforce naming conventions (hooks must start with `use`, components must be PascalCase) — use ESLint with the appropriate plugin +- To write custom TypeScript AST rules in `klint.rules.ts` — that is a separate, code-level workflow diff --git a/.claude/skills/klint-rules b/.claude/skills/klint-rules deleted file mode 120000 index b250658..0000000 --- a/.claude/skills/klint-rules +++ /dev/null @@ -1 +0,0 @@ -../../node_modules/@konvert7/klint/skill/klint-rules \ No newline at end of file diff --git a/.claude/skills/klint-rules/.klint-skill.json b/.claude/skills/klint-rules/.klint-skill.json new file mode 100644 index 0000000..f267932 --- /dev/null +++ b/.claude/skills/klint-rules/.klint-skill.json @@ -0,0 +1,4 @@ +{ + "version": "0.34.0", + "sha256": "c8c9584cee2ef21246fc0d0498e39d7a1af7b965aff875bbb8f55c608004357b" +} diff --git a/.claude/skills/klint-rules/SKILL.md b/.claude/skills/klint-rules/SKILL.md new file mode 100644 index 0000000..0226fef --- /dev/null +++ b/.claude/skills/klint-rules/SKILL.md @@ -0,0 +1,252 @@ +--- +name: klint-rules +description: Add, modify, or explain klint architecture rules for this repo. Use when asked to enforce a new structural constraint, adjust rule scope, or explain why a violation fired. +argument-hint: <constraint to enforce> +--- + +## Schema + +The full, authoritative config schema lives in the repo and is the source of truth for every field below: + +- YAML: <https://github.com/konvert7/klint/blob/main/klint.schema.yaml> +- JSON: <https://github.com/konvert7/klint/blob/main/klint.schema.json> + +Wire it into `klint.yaml` for editor autocomplete and validation: + +```yaml +# yaml-language-server: $schema=./klint.schema.yaml +``` + +## Workflow + +1. **Understand the constraint** — if scope is ambiguous, ask: which files/layers are involved? should it block or warn? are there legitimate exceptions? + +2. **Grep first** — verify the pattern exists and count current occurrences before touching `klint.yaml`: + ```sh + grep -rn "the-pattern" src/ --include="*.ts" + ``` + +3. **Choose the right primitive:** + + | The constraint is... | Primitive | + |---|---| + | Layer X must not import from layer Y | `arch.imports` + `deny` | + | Layer X may only import from Y and Z | `arch.imports` + `allow` | + | Layer X must not import an npm package or `node:` builtin | `arch.imports` + `deny-packages` | + | Pattern P must only appear in one designated file | `arch.singleton` + `pattern` | + | Pattern P must never appear inside a scoped layer | `arch.forbidden` + `pattern` | + | Raw HTML/JSX element must never appear in a scoped layer | `arch.forbidden` + `jsx-element` | + | Raw HTML/JSX element may only appear in one primitive file | `arch.singleton` + `jsx-element` | + | A value-shaped pattern (regex) must never appear in a scoped layer | `arch.forbidden` + `pattern: "re:…"` | + | No file in a layer may exceed N lines | `arch.maxLines` + `limit` | + | No file may be more than N% comments | `arch.maxCommentDensity` + `limit` | + | No comment block may stack more than N lines | `arch.maxCommentBlock` + `limit` | + +4. **Read `klint.yaml`** — check existing `arch.layers` and rules before adding anything. Add a new named layer to `arch.layers` if the file group doesn't exist yet. + +5. **Write the stanza** in `klint.yaml` under the correct `arch:` section. + +6. **Verify zero new violations on the current codebase:** + ```sh + bun klint/cli.ts # in-repo + npx klint # after npm install + ``` + If new violations appear on existing code, fix them first or adjust the rule scope — then land the rule. + +7. **Write a break test** — prove the rule fires on a deliberate violation, and doesn't fire on clean code. Run the test suite to confirm. + +## Primitive reference + +### Layers +```yaml +arch: + layers: + core: ["src/lib/**", "src/hooks/**"] + ui: ["src/components/**"] + targets: ["src/targets/**"] +``` + +### Import boundaries +```yaml +arch: + imports: + # deny: block imports from one layer into another + - from: core + deny: targets + message: "Core must not depend on agent-specific code" + severity: error # optional — default is error; use warn to record without blocking + + # allow: whitelist — anything not listed is denied (npm + node: builtins always pass) + - from: ["src/dao/**"] + allow: ["src/dao/**", "src/prisma/**"] + message: "DAO may only import from dao or prisma" + + # type-only: allow — import type {} and Python if TYPE_CHECKING: blocks stay permitted + - from: core + deny: targets + type-only: allow + message: "Core must not depend on agent-specific code" + + # deny-packages: block npm packages and node: builtins, which deny/allow cannot reach + # because they never resolve to a project file. Composes with deny in the same rule. + - from: ["src/dao/**"] + deny-packages: ["next/headers", "node:fs"] + message: "Data access must not read request state or touch the filesystem" +``` + +`deny-packages` matches per segment: `next` covers `next/headers`, `nextra` is unaffected, and +`next/headers` does not match `next/navigation`. Python uses dotted segments, so `os` covers +`os.path` and blocks pip packages and stdlib modules alike. Swift uses dotted segments too, at +module granularity, which is what makes system frameworks like `UIKit` reachable. Rust splits on +`::`, so `tokio` covers `tokio::sync::Mutex` while `tokio_util` is unaffected. The specifier is read off the AST, so +static imports, dynamic `import()`, and `export … from` re-exports all count while a comment +mentioning the package does not — unlike `forbidden` + `pattern`, which is a line-based text scan. + +### Singleton — one designated location + +Pin a `pattern` (literal string) **or** a `jsx-element` (AST-matched tag) to exactly one file. Every other in-scope occurrence is a violation. + +```yaml +arch: + singleton: + # pattern: literal string match — fires if it appears anywhere except `only` + - pattern: "process.env.API_KEY" + only: "src/lib/auth.ts" + in: ["src/**"] # optional: limit scan scope (default: all files). string OR array + message: "Use the auth module instead of reading API_KEY directly" + severity: error # optional — default error; use warn to record without blocking + + # jsx-element: pin a raw element to its design-system primitive + - jsx-element: "button" # one tag, or a list: ["button", "input"] + only: "src/components/ui/button.tsx" + in: ["src/**/*.tsx"] + message: "Raw <button> belongs only to the Button primitive" +``` + +Required: `only` + `message`, plus one of `pattern` / `jsx-element` (not both in the same stanza). + +### Forbidden — banned pattern in scope + +Block a `pattern` (literal string) **or** a `jsx-element` (AST-matched tag) inside a scoped layer. + +```yaml +arch: + forbidden: + # pattern: literal substring by default; prefix with `re:` for a regex + - pattern: "console.log(" + in: ["src/lib/**"] # string OR array; supports ! negation + message: "Use the logger — console.log leaks into the agent event stream" + severity: error # optional — default error; use warn to record without blocking + + # re: prefix — match a regular expression (common JS/RE2 subset; no lookaround/backrefs) + - pattern: 're:\b(?:p|gap)-\[' + in: ["src/**/*.tsx"] + message: "Use the spacing scale, not arbitrary bracket values like p-[18px]" + + # jsx-element: forbid raw HTML elements outside the design system + - jsx-element: ["button", "input", "label"] + in: ["src/app/**/*.tsx", "src/components/**/*.tsx", "!src/components/ui/**"] + message: "Use the design-system primitives in @/components/ui/* instead of raw HTML elements" +``` + +Required: `in` + `message`, plus one of `pattern` / `jsx-element` (not both in the same stanza). A `pattern` may be a literal substring or, with the `re:` prefix, a regex; `singleton` patterns accept `re:` too. + +`jsx-element` matches intrinsic element names on the AST (opening and self-closing tags), so it is robust to whitespace, attributes, and naming collisions like `<buttonGroup>` — unlike a literal `pattern: "<button"` scan. It works on `.tsx`/`.jsx` files in both the TS and Rust engines. + +### Max lines — cap file length + +Limit how many physical lines a file in scope may have. Use a separate stanza per scope to give different ceilings to source and tests. + +```yaml +arch: + maxLines: + - limit: 300 # positive integer; required + in: ["src/**"] # string OR array; supports ! negation + message: "Split this module" # optional — defaults to "File exceeds the maximum of N lines" + severity: error # optional — default error; use warn to record without blocking + + - limit: 600 # tests may run longer + in: ["tests/**"] +``` + +Required: `limit` + `in`. The count is **total physical lines** — blanks and comments included, not code lines — and a file over the limit is flagged at line `limit + 1`. Enforced identically in the TS and Rust engines, across every language klint scans. + +### Comment budget — cap explanatory prose + +Two ceilings on how much of a file may be comments. Use them to push intent into names and small +functions rather than paragraphs that drift from the code they describe. + +```yaml +arch: + # maxCommentDensity: what share of a file may be comment lines + maxCommentDensity: + - limit: 5 # percent; required + in: ["src/**"] # string OR array; supports ! negation + countDocComments: false # optional — default false, so JSDoc/docstrings are exempt + message: "Encode intent in names and small functions, not prose" + severity: error # optional — default error; use warn to record without blocking + + # maxCommentBlock: how tall a single stack of comment lines may get + maxCommentBlock: + - limit: 2 # consecutive comment lines; required + in: ["src/**"] + ignore: ["@codegen:"] # optional — structural markers, not prose + message: "Extract a well-named function instead of stacking comment lines" +``` + +Required: `limit` + `in`. Density is a **percentage of total physical lines** (code + comments + +blanks — the same denominator `maxLines` uses). Block height counts **consecutive** comment lines and +is reported at the first offending line. Both exempt doc-comments by default — `/** … */` JSDoc and +Python docstrings — so documentation is not penalised; ordinary `//`, `/* */`, and `#` comments always +count. Set `countDocComments: true` to include them. + +The two are complementary: density catches a file that is 30% commented in scattered one-liners, +block catches a ten-line essay above one function in an otherwise sparse file. Neither alone catches both. + +**`ignore` — comments that are machinery, not prose.** Some comments are load-bearing input to a +build or codegen step (`// @codegen:region-start`, feature-strip markers, pragmas). They are comments to the +lexer but structure to the project, and counting them makes both limits measure the wrong thing: + +```yaml +arch: + maxCommentDensity: + - limit: 5 + in: ["src/**"] + ignore: ["@codegen:"] # literal substring, or "re:" for a regex +``` + +Two rules govern how an ignored line behaves, and both matter: + +- **It stays in the density denominator.** A marker is a physical line like code, so a 20-line file + with 4 markers and 1 real comment measures 1/20, not 1/16. +- **It does not break a comment block.** `// a` / `// @marker` / `// b` / `// c` is a run of three + counted lines, not two runs. Otherwise sprinkling a marker every other line would defeat the cap. + +`ignore` tests the **physical source line**, so it only ever applies to lines already identified as +comments — code containing the same text is untouched. Enforced identically in both engines. + +## Pitfalls + +- `pattern` in `singleton` and `forbidden` is a **literal substring** by default — it will not catch `process.env["KEY"]` (bracket notation). Either grep for both forms, or use a `re:` regex prefix to match them in one rule. Caveat: a literal pattern that itself begins with `re:` cannot be expressed (it is always read as a regex), and regexes must stay in the common JS/RE2 subset (no lookaround or backreferences) so both engines agree. +- `maxLines.limit` counts **total physical lines** (blanks and comments included), not code lines; a trailing newline does not add a line, and both engines count identically. +- `maxCommentDensity.limit` is a **percentage, not a line count** — `limit: 5` means 5% of the file, so a 40-line file is over budget at its third comment line. Measure before picking a number: land it as `severity: warn` first, read the reported densities, then tighten to `error`. +- `jsx-element` matches **intrinsic** (lowercase HTML) tags only — `button`, `input`, `label`. It does not match custom React components like `<Button>`; to restrict those, use `arch.imports` against the component's module instead. +- A `forbidden`/`singleton` stanza takes either `pattern` or `jsx-element`, never both — split into two stanzas if you need both. +- `only` in `singleton` is relative to the project root — use forward slashes on all platforms. +- `severity: warn` records the violation but does not block the session. Use `error` (the default) to block. +- Path aliases (`@/*`) resolve via `tsconfig.json` `compilerOptions.paths`, including `extends` chains. +- Adding a rule that fires on existing code is not a blocker — fix the violations first, then add the rule. + +## Output format + +After adding or modifying a rule, report: +- The YAML stanza added +- The grep evidence that confirmed the pattern is real +- The test written and its result +- The output of `bun run klint` (or `npx klint`) confirming zero new violations + +## Do NOT use + +- To fix TypeScript type errors, lint issues, or Biome violations — those are separate tools +- To enforce naming conventions (hooks must start with `use`, components must be PascalCase) — use ESLint with the appropriate plugin +- To write custom TypeScript AST rules in `klint.rules.ts` — that is a separate, code-level workflow diff --git a/.cursor/skills/klint-rules b/.cursor/skills/klint-rules deleted file mode 120000 index b250658..0000000 --- a/.cursor/skills/klint-rules +++ /dev/null @@ -1 +0,0 @@ -../../node_modules/@konvert7/klint/skill/klint-rules \ No newline at end of file diff --git a/.cursor/skills/klint-rules/.klint-skill.json b/.cursor/skills/klint-rules/.klint-skill.json new file mode 100644 index 0000000..f267932 --- /dev/null +++ b/.cursor/skills/klint-rules/.klint-skill.json @@ -0,0 +1,4 @@ +{ + "version": "0.34.0", + "sha256": "c8c9584cee2ef21246fc0d0498e39d7a1af7b965aff875bbb8f55c608004357b" +} diff --git a/.cursor/skills/klint-rules/SKILL.md b/.cursor/skills/klint-rules/SKILL.md new file mode 100644 index 0000000..0226fef --- /dev/null +++ b/.cursor/skills/klint-rules/SKILL.md @@ -0,0 +1,252 @@ +--- +name: klint-rules +description: Add, modify, or explain klint architecture rules for this repo. Use when asked to enforce a new structural constraint, adjust rule scope, or explain why a violation fired. +argument-hint: <constraint to enforce> +--- + +## Schema + +The full, authoritative config schema lives in the repo and is the source of truth for every field below: + +- YAML: <https://github.com/konvert7/klint/blob/main/klint.schema.yaml> +- JSON: <https://github.com/konvert7/klint/blob/main/klint.schema.json> + +Wire it into `klint.yaml` for editor autocomplete and validation: + +```yaml +# yaml-language-server: $schema=./klint.schema.yaml +``` + +## Workflow + +1. **Understand the constraint** — if scope is ambiguous, ask: which files/layers are involved? should it block or warn? are there legitimate exceptions? + +2. **Grep first** — verify the pattern exists and count current occurrences before touching `klint.yaml`: + ```sh + grep -rn "the-pattern" src/ --include="*.ts" + ``` + +3. **Choose the right primitive:** + + | The constraint is... | Primitive | + |---|---| + | Layer X must not import from layer Y | `arch.imports` + `deny` | + | Layer X may only import from Y and Z | `arch.imports` + `allow` | + | Layer X must not import an npm package or `node:` builtin | `arch.imports` + `deny-packages` | + | Pattern P must only appear in one designated file | `arch.singleton` + `pattern` | + | Pattern P must never appear inside a scoped layer | `arch.forbidden` + `pattern` | + | Raw HTML/JSX element must never appear in a scoped layer | `arch.forbidden` + `jsx-element` | + | Raw HTML/JSX element may only appear in one primitive file | `arch.singleton` + `jsx-element` | + | A value-shaped pattern (regex) must never appear in a scoped layer | `arch.forbidden` + `pattern: "re:…"` | + | No file in a layer may exceed N lines | `arch.maxLines` + `limit` | + | No file may be more than N% comments | `arch.maxCommentDensity` + `limit` | + | No comment block may stack more than N lines | `arch.maxCommentBlock` + `limit` | + +4. **Read `klint.yaml`** — check existing `arch.layers` and rules before adding anything. Add a new named layer to `arch.layers` if the file group doesn't exist yet. + +5. **Write the stanza** in `klint.yaml` under the correct `arch:` section. + +6. **Verify zero new violations on the current codebase:** + ```sh + bun klint/cli.ts # in-repo + npx klint # after npm install + ``` + If new violations appear on existing code, fix them first or adjust the rule scope — then land the rule. + +7. **Write a break test** — prove the rule fires on a deliberate violation, and doesn't fire on clean code. Run the test suite to confirm. + +## Primitive reference + +### Layers +```yaml +arch: + layers: + core: ["src/lib/**", "src/hooks/**"] + ui: ["src/components/**"] + targets: ["src/targets/**"] +``` + +### Import boundaries +```yaml +arch: + imports: + # deny: block imports from one layer into another + - from: core + deny: targets + message: "Core must not depend on agent-specific code" + severity: error # optional — default is error; use warn to record without blocking + + # allow: whitelist — anything not listed is denied (npm + node: builtins always pass) + - from: ["src/dao/**"] + allow: ["src/dao/**", "src/prisma/**"] + message: "DAO may only import from dao or prisma" + + # type-only: allow — import type {} and Python if TYPE_CHECKING: blocks stay permitted + - from: core + deny: targets + type-only: allow + message: "Core must not depend on agent-specific code" + + # deny-packages: block npm packages and node: builtins, which deny/allow cannot reach + # because they never resolve to a project file. Composes with deny in the same rule. + - from: ["src/dao/**"] + deny-packages: ["next/headers", "node:fs"] + message: "Data access must not read request state or touch the filesystem" +``` + +`deny-packages` matches per segment: `next` covers `next/headers`, `nextra` is unaffected, and +`next/headers` does not match `next/navigation`. Python uses dotted segments, so `os` covers +`os.path` and blocks pip packages and stdlib modules alike. Swift uses dotted segments too, at +module granularity, which is what makes system frameworks like `UIKit` reachable. Rust splits on +`::`, so `tokio` covers `tokio::sync::Mutex` while `tokio_util` is unaffected. The specifier is read off the AST, so +static imports, dynamic `import()`, and `export … from` re-exports all count while a comment +mentioning the package does not — unlike `forbidden` + `pattern`, which is a line-based text scan. + +### Singleton — one designated location + +Pin a `pattern` (literal string) **or** a `jsx-element` (AST-matched tag) to exactly one file. Every other in-scope occurrence is a violation. + +```yaml +arch: + singleton: + # pattern: literal string match — fires if it appears anywhere except `only` + - pattern: "process.env.API_KEY" + only: "src/lib/auth.ts" + in: ["src/**"] # optional: limit scan scope (default: all files). string OR array + message: "Use the auth module instead of reading API_KEY directly" + severity: error # optional — default error; use warn to record without blocking + + # jsx-element: pin a raw element to its design-system primitive + - jsx-element: "button" # one tag, or a list: ["button", "input"] + only: "src/components/ui/button.tsx" + in: ["src/**/*.tsx"] + message: "Raw <button> belongs only to the Button primitive" +``` + +Required: `only` + `message`, plus one of `pattern` / `jsx-element` (not both in the same stanza). + +### Forbidden — banned pattern in scope + +Block a `pattern` (literal string) **or** a `jsx-element` (AST-matched tag) inside a scoped layer. + +```yaml +arch: + forbidden: + # pattern: literal substring by default; prefix with `re:` for a regex + - pattern: "console.log(" + in: ["src/lib/**"] # string OR array; supports ! negation + message: "Use the logger — console.log leaks into the agent event stream" + severity: error # optional — default error; use warn to record without blocking + + # re: prefix — match a regular expression (common JS/RE2 subset; no lookaround/backrefs) + - pattern: 're:\b(?:p|gap)-\[' + in: ["src/**/*.tsx"] + message: "Use the spacing scale, not arbitrary bracket values like p-[18px]" + + # jsx-element: forbid raw HTML elements outside the design system + - jsx-element: ["button", "input", "label"] + in: ["src/app/**/*.tsx", "src/components/**/*.tsx", "!src/components/ui/**"] + message: "Use the design-system primitives in @/components/ui/* instead of raw HTML elements" +``` + +Required: `in` + `message`, plus one of `pattern` / `jsx-element` (not both in the same stanza). A `pattern` may be a literal substring or, with the `re:` prefix, a regex; `singleton` patterns accept `re:` too. + +`jsx-element` matches intrinsic element names on the AST (opening and self-closing tags), so it is robust to whitespace, attributes, and naming collisions like `<buttonGroup>` — unlike a literal `pattern: "<button"` scan. It works on `.tsx`/`.jsx` files in both the TS and Rust engines. + +### Max lines — cap file length + +Limit how many physical lines a file in scope may have. Use a separate stanza per scope to give different ceilings to source and tests. + +```yaml +arch: + maxLines: + - limit: 300 # positive integer; required + in: ["src/**"] # string OR array; supports ! negation + message: "Split this module" # optional — defaults to "File exceeds the maximum of N lines" + severity: error # optional — default error; use warn to record without blocking + + - limit: 600 # tests may run longer + in: ["tests/**"] +``` + +Required: `limit` + `in`. The count is **total physical lines** — blanks and comments included, not code lines — and a file over the limit is flagged at line `limit + 1`. Enforced identically in the TS and Rust engines, across every language klint scans. + +### Comment budget — cap explanatory prose + +Two ceilings on how much of a file may be comments. Use them to push intent into names and small +functions rather than paragraphs that drift from the code they describe. + +```yaml +arch: + # maxCommentDensity: what share of a file may be comment lines + maxCommentDensity: + - limit: 5 # percent; required + in: ["src/**"] # string OR array; supports ! negation + countDocComments: false # optional — default false, so JSDoc/docstrings are exempt + message: "Encode intent in names and small functions, not prose" + severity: error # optional — default error; use warn to record without blocking + + # maxCommentBlock: how tall a single stack of comment lines may get + maxCommentBlock: + - limit: 2 # consecutive comment lines; required + in: ["src/**"] + ignore: ["@codegen:"] # optional — structural markers, not prose + message: "Extract a well-named function instead of stacking comment lines" +``` + +Required: `limit` + `in`. Density is a **percentage of total physical lines** (code + comments + +blanks — the same denominator `maxLines` uses). Block height counts **consecutive** comment lines and +is reported at the first offending line. Both exempt doc-comments by default — `/** … */` JSDoc and +Python docstrings — so documentation is not penalised; ordinary `//`, `/* */`, and `#` comments always +count. Set `countDocComments: true` to include them. + +The two are complementary: density catches a file that is 30% commented in scattered one-liners, +block catches a ten-line essay above one function in an otherwise sparse file. Neither alone catches both. + +**`ignore` — comments that are machinery, not prose.** Some comments are load-bearing input to a +build or codegen step (`// @codegen:region-start`, feature-strip markers, pragmas). They are comments to the +lexer but structure to the project, and counting them makes both limits measure the wrong thing: + +```yaml +arch: + maxCommentDensity: + - limit: 5 + in: ["src/**"] + ignore: ["@codegen:"] # literal substring, or "re:" for a regex +``` + +Two rules govern how an ignored line behaves, and both matter: + +- **It stays in the density denominator.** A marker is a physical line like code, so a 20-line file + with 4 markers and 1 real comment measures 1/20, not 1/16. +- **It does not break a comment block.** `// a` / `// @marker` / `// b` / `// c` is a run of three + counted lines, not two runs. Otherwise sprinkling a marker every other line would defeat the cap. + +`ignore` tests the **physical source line**, so it only ever applies to lines already identified as +comments — code containing the same text is untouched. Enforced identically in both engines. + +## Pitfalls + +- `pattern` in `singleton` and `forbidden` is a **literal substring** by default — it will not catch `process.env["KEY"]` (bracket notation). Either grep for both forms, or use a `re:` regex prefix to match them in one rule. Caveat: a literal pattern that itself begins with `re:` cannot be expressed (it is always read as a regex), and regexes must stay in the common JS/RE2 subset (no lookaround or backreferences) so both engines agree. +- `maxLines.limit` counts **total physical lines** (blanks and comments included), not code lines; a trailing newline does not add a line, and both engines count identically. +- `maxCommentDensity.limit` is a **percentage, not a line count** — `limit: 5` means 5% of the file, so a 40-line file is over budget at its third comment line. Measure before picking a number: land it as `severity: warn` first, read the reported densities, then tighten to `error`. +- `jsx-element` matches **intrinsic** (lowercase HTML) tags only — `button`, `input`, `label`. It does not match custom React components like `<Button>`; to restrict those, use `arch.imports` against the component's module instead. +- A `forbidden`/`singleton` stanza takes either `pattern` or `jsx-element`, never both — split into two stanzas if you need both. +- `only` in `singleton` is relative to the project root — use forward slashes on all platforms. +- `severity: warn` records the violation but does not block the session. Use `error` (the default) to block. +- Path aliases (`@/*`) resolve via `tsconfig.json` `compilerOptions.paths`, including `extends` chains. +- Adding a rule that fires on existing code is not a blocker — fix the violations first, then add the rule. + +## Output format + +After adding or modifying a rule, report: +- The YAML stanza added +- The grep evidence that confirmed the pattern is real +- The test written and its result +- The output of `bun run klint` (or `npx klint`) confirming zero new violations + +## Do NOT use + +- To fix TypeScript type errors, lint issues, or Biome violations — those are separate tools +- To enforce naming conventions (hooks must start with `use`, components must be PascalCase) — use ESLint with the appropriate plugin +- To write custom TypeScript AST rules in `klint.rules.ts` — that is a separate, code-level workflow diff --git a/bun.lock b/bun.lock index 3cd4b47..d97f00b 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ "@commitlint/cli": "21.0.1", "@commitlint/config-conventional": "21.0.1", "@hughescr/stryker-bun-runner": "1.3.8", - "@konvert7/klint": "0.20.0", + "@konvert7/klint": "0.34.0", "@opencode-ai/plugin": "latest", "@secretlint/secretlint-rule-preset-recommend": "13.0.2", "@semantic-release/changelog": "^6.0.3", @@ -514,15 +514,15 @@ "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], - "@konvert7/klint": ["@konvert7/klint@0.20.0", "", { "dependencies": { "@clack/prompts": "^1.3.0", "typescript": "^5.9.3", "yaml": "^2.9.0", "zod": "^4.4.3" }, "optionalDependencies": { "@konvert7/klint-darwin-arm64": "0.20.0", "@konvert7/klint-darwin-x64": "0.20.0", "@konvert7/klint-linux-x64": "0.20.0", "@konvert7/klint-win32-x64": "0.20.0" }, "bin": { "klint": "cli.ts" } }, "sha512-ASRtYum5DLl6dssGs1Eyrx8nhhTXKVhXhaIr7zFRjJPN1dIDhIbNZh681ivOa4xLpBOSrP6DXk9ZInZviUAW4g=="], + "@konvert7/klint": ["@konvert7/klint@0.34.0", "", { "dependencies": { "@clack/prompts": "^1.3.0", "typescript": "^5.9.3", "yaml": "^2.9.0", "zod": "^4.4.3" }, "optionalDependencies": { "@konvert7/klint-darwin-arm64": "0.34.0", "@konvert7/klint-darwin-x64": "0.34.0", "@konvert7/klint-linux-x64": "0.34.0", "@konvert7/klint-win32-x64": "0.34.0" }, "bin": { "klint": "cli.ts" } }, "sha512-Faef5gItUi2YKoB9RSzwz/lfIlnfYsCcp75XtAhCHz6kpETOLZDZh3rYtUqvjxuQrKhJ9jhkZfetJY7XxOfThg=="], - "@konvert7/klint-darwin-arm64": ["@konvert7/klint-darwin-arm64@0.20.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "klint-rs": "bin/klint-rs" } }, "sha512-OiCo09bHZ+GFW3amRWhdloVKsFKaHJxxdzaSeAGsN1M4sEgOFIYIY5BwrNmi0g9G11i2wkFCOL6W4m+kU5vWGw=="], + "@konvert7/klint-darwin-arm64": ["@konvert7/klint-darwin-arm64@0.34.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "klint-rs": "bin/klint-rs" } }, "sha512-KVv3eSzmzvAGEvRvrLlm4WnT6saMe4cH8m4i0YtVX1wC0k4eSgug4v2TIRYjg2IyDC/mWq14yKM2h2+b9IK/6w=="], - "@konvert7/klint-darwin-x64": ["@konvert7/klint-darwin-x64@0.20.0", "", { "os": "darwin", "cpu": "x64", "bin": { "klint-rs": "bin/klint-rs" } }, "sha512-lZgROaZ0ArDlRG96jJlPVXlqFtam9X7FyDFTkXYTy8o5Y62uaDvXSptS27nv7fRn/NPR38YXn+TaqOQLspFn/g=="], + "@konvert7/klint-darwin-x64": ["@konvert7/klint-darwin-x64@0.34.0", "", { "os": "darwin", "cpu": "x64", "bin": { "klint-rs": "bin/klint-rs" } }, "sha512-TVM1vXWRTcooNBS6JBYQ35lcg29p8TqzhrM0R/+7hrNZKo/9Y5vcKjoHLwlgLFryAu+nIzh1AtTAiiTeD4mrkg=="], - "@konvert7/klint-linux-x64": ["@konvert7/klint-linux-x64@0.20.0", "", { "os": "linux", "cpu": "x64", "bin": { "klint-rs": "bin/klint-rs" } }, "sha512-fYWtTDNK5JD4aTRZrkXMRzXCPzHCx9bKUKM+Ivq88urbFVUu04TVaqFNfGpAX1A60BRnqduAtjQ7RtAti+/tZQ=="], + "@konvert7/klint-linux-x64": ["@konvert7/klint-linux-x64@0.34.0", "", { "os": "linux", "cpu": "x64", "bin": { "klint-rs": "bin/klint-rs" } }, "sha512-uvWvUUwvbRG6HbMqmx8y/yrcSNe59rD0/Up/mFdc6BgeweKZf5omdG8Xv2jkVwxa8Ywvc9003gIFPlFVmoQqEA=="], - "@konvert7/klint-win32-x64": ["@konvert7/klint-win32-x64@0.20.0", "", { "os": "win32", "cpu": "x64", "bin": { "klint-rs": "bin/klint-rs.exe" } }, "sha512-pEaL9HwyjkDZit16ZWHJ7Qg5DoY5T+OYV3XOO3xCoy+g9bpNpLsIDSofsq2tw8UrR39uGnc6hVYz0st9ltUyiA=="], + "@konvert7/klint-win32-x64": ["@konvert7/klint-win32-x64@0.34.0", "", { "os": "win32", "cpu": "x64", "bin": { "klint-rs": "bin/klint-rs.exe" } }, "sha512-BIwN0deVVo3wvTCjRAAOmzl6QCUOyF32O6aq4lhLQC3Tt9lTpex49MuER9QH1fwRBToq9F9KObEGqytcRUqEfw=="], "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], diff --git a/package.json b/package.json index 35b25ab..8062fd6 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@commitlint/cli": "21.0.1", "@commitlint/config-conventional": "21.0.1", "@hughescr/stryker-bun-runner": "1.3.8", - "@konvert7/klint": "0.20.0", + "@konvert7/klint": "0.34.0", "@opencode-ai/plugin": "latest", "@secretlint/secretlint-rule-preset-recommend": "13.0.2", "@semantic-release/changelog": "^6.0.3", From 3139113a7ee4ff537f572aad430fb9bfdb94cd04 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 15:52:02 +0200 Subject: [PATCH 26/35] fix(hooks): claim a pending failure inside the state directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename within PAL_HOME so the claim survives a temp dir on another volume - keep the atomic claim that stops two concurrent stop hooks racing Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/hooks/lib/stop.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hooks/lib/stop.ts b/src/hooks/lib/stop.ts index 7d866ff..a1d21f8 100644 --- a/src/hooks/lib/stop.ts +++ b/src/hooks/lib/stop.ts @@ -3,6 +3,7 @@ * Used by StopOrchestrator.ts (Claude Code) and opencode plugin. */ +import { randomUUID } from "node:crypto"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { mkdtemp, rename, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -189,8 +190,10 @@ async function detachFailurePrinciple(transcript: string): Promise<void> { // Rename to claim the pending file atomically — prevents two Stop hooks // racing on the same low rating (opencode notably fires session.idle AND // session.diff concurrently, so runStopHandlers runs twice in parallel). - const claimedDir = await mkdtemp(resolve(tmpdir(), "pal-pending-")); - const claimedPath = resolve(claimedDir, "pending.json"); + // The claim stays inside the state directory: rename fails with EXDEV across + // devices, and the OS temp dir is on another volume often enough to matter. + const claimId: string = randomUUID(); + const claimedPath = resolve(paths.state(), `pending-failure.${claimId}.json`); try { await rename(pendingPath, claimedPath); } catch (err) { From 14432c77b18e375b3a42ce0bc283cfae49f64890 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 16:10:16 +0200 Subject: [PATCH 27/35] ci: pin actions to commit shas and install without lifecycle scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pin every action to the commit its version tag resolves to, annotated with that version - install dependencies with --ignore-scripts so no package runs code at install time Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .github/workflows/ci.yml | 25 ++++++++++++++----------- .github/workflows/mutation.yml | 9 +++++---- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60affd7..903ce68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,15 +31,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: "1.3.13" - name: Install dependencies - run: bun install --frozen-lockfile + # No lifecycle scripts, so no dependency can run arbitrary code on install. + run: bun install --frozen-lockfile --ignore-scripts - name: Audit dependencies # Informational only — bun audit lacks a --production flag, and all @@ -90,15 +91,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: "1.3.13" - name: Install dependencies - run: bun install --frozen-lockfile + # No lifecycle scripts, so no dependency can run arbitrary code on install. + run: bun install --frozen-lockfile --ignore-scripts - name: Pack tarball shell: bash @@ -158,29 +160,30 @@ jobs: steps: - name: Generate GitHub App Token id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.APP_CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} persist-credentials: true - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 registry-url: https://registry.npmjs.org - name: Install dependencies - run: bun install --frozen-lockfile + # No lifecycle scripts, so no dependency can run arbitrary code on install. + run: bun install --frozen-lockfile --ignore-scripts - name: Release run: bunx semantic-release diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index edb9a63..26df32b 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -23,27 +23,28 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: # Full history so the merge-base against the PR base branch resolves. fetch-depth: 0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: # Hard pin, not a preference: bun 1.3.14 changed async-promise cleanup in a # way that breaks the Stryker bun runner. Do not float this to latest. bun-version: "1.3.13" - name: Install dependencies - run: bun install --frozen-lockfile + # No lifecycle scripts, so no dependency can run arbitrary code on install. + run: bun install --frozen-lockfile --ignore-scripts - name: Mutation-test the changed lines run: bun run test:mutate:diff "origin/${{ github.event.pull_request.base.ref }}" - name: Upload mutation report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: mutation-report path: reports/mutation.html From 5e464dacecba8688b68d9207b59a55d765af8032 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 16:19:31 +0200 Subject: [PATCH 28/35] chore(copilot): gate the session end for Copilot too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add .github/hooks/gates.json running the same eight checks on agentStop - document the Codex and Copilot wiring alongside the other agents - bring the gate table and chain listing in line with what the hooks now run Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- .github/hooks/gates.json | 47 ++++++++++++++++++++++++++++++++++++++++ AGENTS.md | 45 ++++++++++++++++++++++++++++---------- 2 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 .github/hooks/gates.json diff --git a/.github/hooks/gates.json b/.github/hooks/gates.json new file mode 100644 index 0000000..34867e8 --- /dev/null +++ b/.github/hooks/gates.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "hooks": { + "agentStop": [ + { + "type": "command", + "command": "bun .agents/hooks/check.ts", + "timeoutSec": 120 + }, + { + "type": "command", + "command": "bun .agents/hooks/type-check.ts", + "timeoutSec": 120 + }, + { + "type": "command", + "command": "bun .agents/hooks/knip.ts", + "timeoutSec": 120 + }, + { + "type": "command", + "command": "bun .agents/hooks/jscpd.ts", + "timeoutSec": 120 + }, + { + "type": "command", + "command": "bun .agents/hooks/klint.ts", + "timeoutSec": 120 + }, + { + "type": "command", + "command": "bun .agents/hooks/secretlint.ts", + "timeoutSec": 120 + }, + { + "type": "command", + "command": "bun .agents/hooks/madge.ts", + "timeoutSec": 120 + }, + { + "type": "command", + "command": "bun .agents/hooks/lf.ts", + "timeoutSec": 120 + } + ] + } +} diff --git a/AGENTS.md b/AGENTS.md index 5617190..69fc5ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,11 +20,17 @@ When PAL is installed, `~/.pal/skills/<name>/` may be a directory junction back ``` . ├── .agents/ # this repo's dev hooks (not the PAL runtime hooks) -│ └── hooks/ # bun-run TypeScript hooks called by Stop events +│ ├── hooks/ # bun-run TypeScript hooks called by Stop events +│ └── scripts/ # helpers the hooks and package scripts call ├── .claude/ # project-level Claude Code config (committed) │ └── settings.json # Stop hook + permission allowlist +├── .codex/ # project-level Codex config (committed) +│ └── hooks.json # Stop event → same hook chain, --codex output ├── .cursor/ # project-level Cursor config (committed) │ └── hooks.json # stop event → same hook chain +├── .github/ +│ ├── hooks/gates.json # Copilot agentStop → same hook chain +│ └── workflows/ # ci + mutation pipelines ├── .opencode/ # opencode plugin folder (committed) │ └── plugins/lint.ts # session.idle handler → same hook chain ├── .husky/ # pre-commit / pre-push hooks (lint-staged + biome) @@ -83,15 +89,22 @@ bun test test/doctor.test.ts bun test --test-name-pattern "scaffolds telos" ``` -Check, typecheck, knip, test — each script and its agent-hook wrapper: - -| Script | What it does | Hook wrapper | -| --------------------- | --------------------------------------- | ------------------------------ | -| `bun run check` | Biome lint + format (read-only) | `.agents/hooks/check.ts` | -| `bun run check-write` | Biome lint + format with `--write` | (manual, not in stop chain) | -| `bun run type-check` | `tsc --noEmit` | `.agents/hooks/type-check.ts` | -| `bun run knip` | Knip dead-code / unused-deps scan | `.agents/hooks/knip.ts` | -| `bun run test` | Bun test runner | (run by CI; not in stop chain) | +Each gate script and its agent-hook wrapper: + +| Script | What it does | Hook wrapper | +| --------------------- | ---------------------------------------- | ------------------------------ | +| `bun run check` | Biome lint + format (read-only) | `.agents/hooks/check.ts` | +| `bun run check-write` | Biome lint + format with `--write` | (manual, not in stop chain) | +| `bun run type-check` | `tsc --noEmit` | `.agents/hooks/type-check.ts` | +| `bun run knip` | Knip dead-code / unused-deps scan | `.agents/hooks/knip.ts` | +| `bun run jscpd` | Copy-paste detection | `.agents/hooks/jscpd.ts` | +| `bun run klint` | Architecture rules | `.agents/hooks/klint.ts` | +| `bun run madge` | Circular-import detection | `.agents/hooks/madge.ts` | +| `bun run lf` | CRLF guard on tracked files | `.agents/hooks/lf.ts` | +| `bun run lf:fix` | Converts offenders to LF | (manual, not in stop chain) | +| `bun run secretlint` | Secret scanning | `.agents/hooks/secretlint.ts` | +| `bun run test` | Bun test runner (randomized) | (pre-push and CI; not in stop) | +| `bun run test:mutate:diff` | Stryker on the changed lines | (CI on pull requests) | Run the CLI itself in dev mode against a sandboxed home: @@ -104,14 +117,22 @@ PAL_HOME=./.test-home bun src/cli/index.ts cli init ## The `.agents/hooks/` system -All three checks above are wired into the **Stop / session-end** event of every agent that has a config file in this repo. When an agent stops generating in this repo, it runs: +Every gate above is wired into the **Stop / session-end** event of each agent that has a config file in this repo. When an agent stops generating, it runs: ``` bun .agents/hooks/check.ts bun .agents/hooks/type-check.ts bun .agents/hooks/knip.ts +bun .agents/hooks/jscpd.ts +bun .agents/hooks/klint.ts +bun .agents/hooks/secretlint.ts +bun .agents/hooks/madge.ts +bun .agents/hooks/lf.ts ``` +`run-hook.ts` skips the whole chain when `git status` is empty, so a conversational +turn that changed nothing costs a single git call. + If any exits non-zero, the agent treats the session-end as blocked and the failure output is surfaced — the agent must fix the underlying issue before it can stop. Per-agent wiring: @@ -120,6 +141,8 @@ Per-agent wiring: | ----------- | ---------------------------- | -------------- | | Claude Code | `.claude/settings.json` | `Stop` | | Cursor | `.cursor/hooks.json` | `stop` | +| Codex | `.codex/hooks.json` | `Stop` | +| Copilot | `.github/hooks/gates.json` | `agentStop` | | opencode | `.opencode/plugins/lint.ts` | `session.idle` | Each individual hook file is a thin wrapper: From 07896c8afdfc4a80cbcd699574f24ec319f7c560 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 18:03:11 +0200 Subject: [PATCH 29/35] feat(machine): give each install a stable identity that records can reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records that cross machines need an origin, and a name for that origin has to survive being renamed. This adds the identity without touching any writer yet. ~/.pal/machine.json holds { id, label, os, createdAt }. The id is a uuid, generated once and never regenerated: discarding one orphans every record that referenced it. A stored file carrying a usable id is therefore repaired rather than replaced when other fields are missing or corrupt. The default label is derived from the id, deliberately not from the hostname — a hostname routinely carries the owner's real name, and labels travel inside every exported registry entry. Records store the id and never the label. displayName() resolves id to label at read time against memory/machines/<id>.md, so renaming a machine is a one-file edit no stored record notices, and two machines sharing a label is a display concern: the name is suffixed with the short id only when the registry actually shows a collision. Uniqueness is never enforced, because the registry is not always reachable. An unknown id degrades to its short form rather than throwing. machine.json sits at the PAL_HOME root, outside every exported directory, and is denied at the import boundary. Registry entries live under memory/ and do travel, which is how a foreign id becomes a name. Export archives also carry export-manifest.json naming the producing machine — after a merge an archive can hold entries for several machines, so the manifest says which one made this one. Import records it in the import log. Importing now registers the receiving machine too: a box that only ever imports previously had no identity at all. Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/cli/index.ts | 17 ++- src/hooks/lib/export.ts | 39 ++++- src/hooks/lib/import-merge.ts | 31 +++- src/hooks/lib/machine.ts | 176 ++++++++++++++++++++++ test/import-merge.test.ts | 23 +++ test/machine.test.ts | 272 ++++++++++++++++++++++++++++++++++ 6 files changed, 555 insertions(+), 3 deletions(-) create mode 100644 src/hooks/lib/machine.ts create mode 100644 test/machine.test.ts diff --git a/src/cli/index.ts b/src/cli/index.ts index 167053d..b97dc3f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -35,9 +35,15 @@ import { } from "node:fs"; import { homedir } from "node:os"; import { resolve } from "node:path"; -import { appendImportLog, mergeArchive, summarize } from "../hooks/lib/import-merge"; +import { + appendImportLog, + mergeArchive, + readManifest, + summarize, +} from "../hooks/lib/import-merge"; import { inference, previewInferenceRoute } from "../hooks/lib/inference"; import { DEBUG_LOG_MAX_ROTATED, logDebug } from "../hooks/lib/log"; +import { ensureRegistered, writeRegistryEntry } from "../hooks/lib/machine"; import { palHome, palPkg, paths, platform } from "../hooks/lib/paths"; import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup"; import { log } from "../targets/lib"; @@ -1403,6 +1409,7 @@ async function importState(args: string[]) { return; } + ensureRegistered(home); const quarantineDir = resolve( home, "backups", @@ -1416,6 +1423,13 @@ async function importState(args: string[]) { home, quarantineDir ); + const source = readManifest( + entries.map((e) => ({ path: e.entryName, data: () => e.getData() })) + ); + if (source) { + writeRegistryEntry({ id: source.machineId, label: source.label, os: source.os }); + console.log(`Source machine: ${source.label} (${source.machineId})`); + } appendImportLog(home, { ts: new Date().toISOString(), @@ -1428,6 +1442,7 @@ async function importState(args: string[]) { skipped: result.skipped.length, linesAdded: result.linesAdded, quarantineDir: result.quarantineDir, + sourceMachineId: source?.machineId ?? null, }); logDebug("import", `done merge ${summarize(result)} to=${home}`); diff --git a/src/hooks/lib/export.ts b/src/hooks/lib/export.ts index 0a8e1a6..db0fda8 100644 --- a/src/hooks/lib/export.ts +++ b/src/hooks/lib/export.ts @@ -6,6 +6,7 @@ import { existsSync, readdirSync } from "node:fs"; import { relative, resolve } from "node:path"; import AdmZip from "adm-zip"; +import { ensureRegistered } from "./machine"; import { palHome } from "./paths"; /** @@ -59,9 +60,41 @@ export function collectExportFiles(): string[] { return files; } -/** Zip the given files and write to outputPath. Returns file count. */ +/** Archive metadata naming the machine that produced it. */ +export const MANIFEST_NAME = "export-manifest.json"; + +export interface ExportManifest { + machineId: string; + label: string; + os: string; + exportedAt: string; + fileCount: number; +} + +export function buildManifest( + identity: { id: string; label: string; os: string }, + fileCount: number +): ExportManifest { + return { + machineId: identity.id, + label: identity.label, + os: identity.os, + exportedAt: new Date().toISOString(), + fileCount, + }; +} + +/** + * Zip the given files and write to outputPath. Returns file count. + * + * The archive declares its source machine in a manifest. Registry entries under + * memory/machines/ travel with the corpus, so the manifest exists to say which + * machine produced THIS archive — after a merge an archive can carry entries + * for several machines. + */ export function exportZip(outputPath: string): number { const root = palHome(); + const identity = ensureRegistered(root); const files = collectExportFiles(); if (files.length === 0) return 0; @@ -71,6 +104,10 @@ export function exportZip(outputPath: string): number { const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : ""; zip.addLocalFile(fullPath, dir); } + zip.addFile( + MANIFEST_NAME, + Buffer.from(`${JSON.stringify(buildManifest(identity, files.length), null, 2)}\n`) + ); zip.writeZip(outputPath); return files.length; diff --git a/src/hooks/lib/import-merge.ts b/src/hooks/lib/import-merge.ts index d88b1c6..dca75c5 100644 --- a/src/hooks/lib/import-merge.ts +++ b/src/hooks/lib/import-merge.ts @@ -18,6 +18,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; +import { type ExportManifest, MANIFEST_NAME } from "./export"; /** One file inside an export archive, decoupled from the zip library. */ export interface ArchiveEntry { @@ -40,7 +41,11 @@ export interface MergeResult { * identity — importing it would give two machines one id and silently break * every origin-scoped read. The retrieval index is rebuilt from its sources. */ -const NEVER_IMPORT = ["machine.json", "memory/learning/.retrieval-index.json"]; +const NEVER_IMPORT = [ + "machine.json", + "export-manifest.json", + "memory/learning/.retrieval-index.json", +]; function normalize(path: string): string { return path.replaceAll("\\", "/").replace(/^\.\//, ""); @@ -154,6 +159,29 @@ export function mergeArchive( return result; } +/** + * The source machine declared by an archive's manifest, or null when the + * archive predates manifests. + */ +export function readManifest(entries: ArchiveEntry[]): ExportManifest | null { + const hit = entries.find((e) => normalize(e.path) === MANIFEST_NAME); + if (!hit) return null; + try { + const parsed = JSON.parse(hit.data().toString("utf-8")) as Partial<ExportManifest>; + if (typeof parsed.machineId !== "string" || parsed.machineId.length === 0) + return null; + return { + machineId: parsed.machineId, + label: parsed.label ?? parsed.machineId, + os: parsed.os ?? "", + exportedAt: parsed.exportedAt ?? "", + fileCount: parsed.fileCount ?? 0, + }; + } catch { + return null; + } +} + export interface ImportLogEntry { ts: string; archive: string; @@ -165,6 +193,7 @@ export interface ImportLogEntry { skipped: number; linesAdded: number; quarantineDir: string | null; + sourceMachineId?: string | null; } /** Append one record per import so a merged corpus stays attributable. */ diff --git a/src/hooks/lib/machine.ts b/src/hooks/lib/machine.ts new file mode 100644 index 0000000..ddb0cb5 --- /dev/null +++ b/src/hooks/lib/machine.ts @@ -0,0 +1,176 @@ +/** + * Machine identity — who this install is, and how a record's origin becomes a + * name at display time. + * + * Records store the id and never the label. Resolution happens on read, so + * renaming a machine is a one-file edit that no stored record notices, and two + * machines sharing a label is a display concern rather than a data collision. + * + * `machine.json` lives at the PAL_HOME root, outside every exported directory, + * because importing it would give two installs one id and silently break every + * origin-scoped read built on top of it. + */ + +import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { platform as osPlatform } from "node:os"; +import { resolve } from "node:path"; +import { parse, stringify } from "./frontmatter"; +import { palHome, paths } from "./paths"; + +export interface MachineIdentity { + id: string; + label: string; + os: string; + createdAt: string; +} + +const SHORT_ID_LENGTH = 4; + +export function machineFilePath(home: string = palHome()): string { + return resolve(home, "machine.json"); +} + +function machinesDir(): string { + const dir = resolve(paths.memory(), "machines"); + mkdirSync(dir, { recursive: true }); + return dir; +} + +/** First segment of the uuid — enough to disambiguate two same-labelled machines. */ +export function shortId(id: string): string { + return id.replaceAll("-", "").slice(0, SHORT_ID_LENGTH); +} + +/** + * Neutral default label. Deliberately not derived from the hostname: a hostname + * routinely carries the owner's real name, and the label travels in every + * exported registry entry. + */ +export function defaultLabel(id: string): string { + return `machine-${shortId(id)}`; +} + +function newIdentity(): MachineIdentity { + const id = crypto.randomUUID(); + return { + id, + label: defaultLabel(id), + os: osPlatform(), + createdAt: new Date().toISOString(), + }; +} + +function hasUsableId(value: unknown): value is Partial<MachineIdentity> & { id: string } { + const v = value as Partial<MachineIdentity> | null; + return typeof v?.id === "string" && v.id.length > 0; +} + +/** + * Fill in whatever a stored identity is missing. Only the id is irreplaceable — + * discarding one orphans every record that referenced it — so a file carrying a + * usable id is repaired rather than regenerated. + */ +function repair(stored: Partial<MachineIdentity> & { id: string }): MachineIdentity { + return { + id: stored.id, + label: stored.label?.trim() || defaultLabel(stored.id), + os: stored.os || osPlatform(), + createdAt: stored.createdAt || new Date().toISOString(), + }; +} + +/** + * This install's identity, created on first call and stable afterwards. The id + * is never regenerated once the file exists — a changed id orphans every record + * that referenced the old one. + */ +export function loadMachine(home: string = palHome()): MachineIdentity { + const file = machineFilePath(home); + if (existsSync(file)) { + try { + const parsed = JSON.parse(readFileSync(file, "utf-8")) as unknown; + if (hasUsableId(parsed)) return repair(parsed); + } catch { + /* fall through to regeneration below */ + } + } + const identity = newIdentity(); + mkdirSync(home, { recursive: true }); + writeFileSync(file, `${JSON.stringify(identity, null, 2)}\n`); + return identity; +} + +/** Rename this machine. No stored record is touched — labels resolve on read. */ +export function setLabel(label: string, home: string = palHome()): MachineIdentity { + const current = loadMachine(home); + const updated = { ...current, label: label.trim() || current.label }; + writeFileSync(machineFilePath(home), `${JSON.stringify(updated, null, 2)}\n`); + return updated; +} + +export interface RegistryEntry { + id: string; + label: string; + os: string; +} + +function registryPath(id: string): string { + return resolve(machinesDir(), `${id}.md`); +} + +/** Write (or refresh) a machine's registry entry. Registry entries are exported. */ +export function writeRegistryEntry(entry: RegistryEntry, body = ""): string { + const file = registryPath(entry.id); + const existingBody = existsSync(file) ? parse(readFileSync(file, "utf-8")).body : ""; + const content = stringify( + { + id: entry.id, + label: entry.label, + os: entry.os, + updated: new Date().toISOString(), + }, + body || existingBody + ); + writeFileSync(file, content); + return file; +} + +/** Every known machine, this one and any that arrived via import. */ +export function readRegistry(): RegistryEntry[] { + const dir = machinesDir(); + const entries: RegistryEntry[] = []; + for (const name of readdirSync(dir)) { + if (!name.endsWith(".md")) continue; + try { + const meta = parse<Record<string, string>>( + readFileSync(resolve(dir, name), "utf-8") + ).meta; + if (meta.id && meta.label) { + entries.push({ id: meta.id, label: meta.label, os: meta.os ?? "" }); + } + } catch { + /* a malformed entry must not hide the rest of the registry */ + } + } + return entries; +} + +/** + * Name for a record's origin id. Unknown ids fall back to the short id so a + * record from a machine whose entry has not arrived yet still reads sensibly. + * A label shared by two machines is suffixed rather than deduplicated — the + * registry is not always reachable, so uniqueness can never be enforced. + */ +export function displayName(id: string, registry: RegistryEntry[]): string { + const entry = registry.find((e) => e.id === id); + if (!entry) return shortId(id); + const sharesLabel = registry.some((e) => e.id !== id && e.label === entry.label); + return sharesLabel ? `${entry.label}·${shortId(id)}` : entry.label; +} + +/** Register this install so its label can be resolved on any machine. */ +export function ensureRegistered(home: string = palHome()): MachineIdentity { + const identity = loadMachine(home); + writeRegistryEntry({ id: identity.id, label: identity.label, os: identity.os }); + return identity; +} diff --git a/test/import-merge.test.ts b/test/import-merge.test.ts index adf3efd..115fc8b 100644 --- a/test/import-merge.test.ts +++ b/test/import-merge.test.ts @@ -216,3 +216,26 @@ describe("isNeverImport", () => { expect(isNeverImport("skills/my-skill/SKILL.md")).toBe(false); }); }); + +describe("machine identity crosses on import", () => { + test("source machine is named and its label resolves afterwards", async () => { + cli(SRC, ["export", WORK]); + const srcId = JSON.parse(readFileSync(resolve(SRC, "machine.json"), "utf-8")) + .id as string; + + const r = cli(DST, ["import", WORK]); + expect(r.status).toBe(0); + expect(r.stdout).toContain("Source machine:"); + + // DST keeps its own identity... + const dstId = JSON.parse(readFileSync(resolve(DST, "machine.json"), "utf-8")) + .id as string; + expect(dstId).not.toBe(srcId); + + // ...and can now name the machine those records came from. + expect(existsSync(resolve(DST, "memory", "machines", `${srcId}.md`))).toBe(true); + + const logged = lines(DST, "memory/state/import-log.jsonl").map((l) => JSON.parse(l)); + expect(logged[0].sourceMachineId).toBe(srcId); + }); +}); diff --git a/test/machine.test.ts b/test/machine.test.ts new file mode 100644 index 0000000..c7e402d --- /dev/null +++ b/test/machine.test.ts @@ -0,0 +1,272 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; + +let HOME: string; + +beforeEach(() => { + HOME = mkdtempSync(resolve(tmpdir(), "pal-machine-")); + process.env.PAL_HOME = HOME; +}); + +afterEach(() => { + delete process.env.PAL_HOME; + rmSync(HOME, { recursive: true, force: true }); +}); + +async function lib() { + return await import("../src/hooks/lib/machine"); +} + +describe("loadMachine", () => { + test("creates machine.json on first call with a uuid", async () => { + const { loadMachine, machineFilePath } = await lib(); + const m = loadMachine(HOME); + expect(m.id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + ); + expect(existsSync(machineFilePath(HOME))).toBe(true); + }); + + test("returns the same id on every subsequent call", async () => { + const { loadMachine } = await lib(); + const first = loadMachine(HOME); + const second = loadMachine(HOME); + const third = loadMachine(HOME); + expect(second.id).toBe(first.id); + expect(third.id).toBe(first.id); + }); + + test("records os and a creation timestamp", async () => { + const { loadMachine } = await lib(); + const m = loadMachine(HOME); + expect(m.os.length).toBeGreaterThan(0); + expect(Number.isNaN(Date.parse(m.createdAt))).toBe(false); + }); + + test("regenerates when the file is corrupt rather than throwing", async () => { + const { loadMachine, machineFilePath } = await lib(); + writeFileSync(machineFilePath(HOME), "not json at all"); + const m = loadMachine(HOME); + expect(m.id.length).toBeGreaterThan(0); + }); + + test("regenerates when the file is valid json but missing an id", async () => { + const { loadMachine, machineFilePath } = await lib(); + writeFileSync(machineFilePath(HOME), JSON.stringify({ label: "orphan" })); + const m = loadMachine(HOME); + expect(m.id.length).toBeGreaterThan(0); + expect(m.label).not.toBe("orphan"); + }); + + test("REPAIRS rather than regenerates when the id survives but fields are missing", async () => { + const { loadMachine, machineFilePath, defaultLabel } = await lib(); + writeFileSync(machineFilePath(HOME), JSON.stringify({ id: "kept-id" })); + const m = loadMachine(HOME); + expect(m.id).toBe("kept-id"); + expect(m.label).toBe(defaultLabel("kept-id")); + expect(m.os.length).toBeGreaterThan(0); + expect(Number.isNaN(Date.parse(m.createdAt))).toBe(false); + }); + + test("keeps a repaired id stable across reloads", async () => { + const { loadMachine, machineFilePath } = await lib(); + writeFileSync(machineFilePath(HOME), JSON.stringify({ id: "kept-id" })); + expect(loadMachine(HOME).id).toBe("kept-id"); + expect(loadMachine(HOME).id).toBe("kept-id"); + }); +}); + +describe("defaultLabel", () => { + test("is derived from the id, never from the hostname", async () => { + const { defaultLabel } = await lib(); + const label = defaultLabel("9f2c8e14-1111-2222-3333-444455556666"); + expect(label).toBe("machine-9f2c"); + expect(label).not.toContain(require("node:os").hostname()); + }); + + test("shortId takes four hex chars and ignores dashes", async () => { + const { shortId } = await lib(); + expect(shortId("9f2c8e14-1111-2222-3333-444455556666")).toBe("9f2c"); + expect(shortId("ab-cd-ef-01")).toBe("abcd"); + }); +}); + +describe("setLabel", () => { + test("changes the label while keeping the id", async () => { + const { loadMachine, setLabel } = await lib(); + const before = loadMachine(HOME); + const after = setLabel("workstation", HOME); + expect(after.label).toBe("workstation"); + expect(after.id).toBe(before.id); + }); + + test("persists across reloads", async () => { + const { loadMachine, setLabel } = await lib(); + loadMachine(HOME); + setLabel("workstation", HOME); + expect(loadMachine(HOME).label).toBe("workstation"); + }); + + test("ignores an empty label instead of clearing the name", async () => { + const { loadMachine, setLabel } = await lib(); + const before = setLabel("workstation", HOME); + const after = setLabel(" ", HOME); + expect(after.label).toBe(before.label); + expect(loadMachine(HOME).label).toBe("workstation"); + }); +}); + +describe("registry", () => { + test("ensureRegistered writes an entry under memory/machines", async () => { + const { ensureRegistered } = await lib(); + const m = ensureRegistered(HOME); + expect(existsSync(resolve(HOME, "memory", "machines", `${m.id}.md`))).toBe(true); + }); + + test("readRegistry returns every written machine", async () => { + const { writeRegistryEntry, readRegistry } = await lib(); + writeRegistryEntry({ id: "id-a", label: "alpha", os: "linux" }); + writeRegistryEntry({ id: "id-b", label: "beta", os: "darwin" }); + const reg = readRegistry(); + expect(reg.map((e) => e.label).sort()).toEqual(["alpha", "beta"]); + }); + + test("re-registering updates the label and does not duplicate the entry", async () => { + const { writeRegistryEntry, readRegistry } = await lib(); + writeRegistryEntry({ id: "id-a", label: "alpha", os: "linux" }); + writeRegistryEntry({ id: "id-a", label: "renamed", os: "linux" }); + const reg = readRegistry(); + expect(reg.length).toBe(1); + expect(reg[0].label).toBe("renamed"); + }); + + test("preserves the entry body across a label change", async () => { + const { writeRegistryEntry } = await lib(); + writeRegistryEntry({ id: "id-a", label: "alpha", os: "linux" }, "roots: /home/x"); + writeRegistryEntry({ id: "id-a", label: "renamed", os: "linux" }); + const raw = readFileSync(resolve(HOME, "memory", "machines", "id-a.md"), "utf-8"); + expect(raw).toContain("roots: /home/x"); + expect(raw).toContain("renamed"); + }); + + test("skips a malformed entry without hiding the rest", async () => { + const { writeRegistryEntry, readRegistry } = await lib(); + writeRegistryEntry({ id: "id-a", label: "alpha", os: "linux" }); + mkdirSync(resolve(HOME, "memory", "machines"), { recursive: true }); + writeFileSync( + resolve(HOME, "memory", "machines", "broken.md"), + "no frontmatter here" + ); + const reg = readRegistry(); + expect(reg.length).toBe(1); + expect(reg[0].label).toBe("alpha"); + }); +}); + +describe("displayName", () => { + test("resolves a known id to its label", async () => { + const { displayName } = await lib(); + const reg = [{ id: "id-a", label: "macbook", os: "darwin" }]; + expect(displayName("id-a", reg)).toBe("macbook"); + }); + + test("does NOT suffix when the label is unique", async () => { + const { displayName } = await lib(); + const reg = [ + { id: "id-a", label: "macbook", os: "darwin" }, + { id: "id-b", label: "desktop", os: "linux" }, + ]; + expect(displayName("id-a", reg)).toBe("macbook"); + }); + + test("suffixes with the short id when two machines share a label", async () => { + const { displayName } = await lib(); + const reg = [ + { id: "aaaa1111-0000-0000-0000-000000000000", label: "macbook", os: "darwin" }, + { id: "bbbb2222-0000-0000-0000-000000000000", label: "macbook", os: "darwin" }, + ]; + expect(displayName("aaaa1111-0000-0000-0000-000000000000", reg)).toBe("macbook·aaaa"); + expect(displayName("bbbb2222-0000-0000-0000-000000000000", reg)).toBe("macbook·bbbb"); + }); + + test("falls back to the short id when the machine is unknown", async () => { + const { displayName } = await lib(); + expect(displayName("ffff9999-0000-0000-0000-000000000000", [])).toBe("ffff"); + }); +}); + +describe("machine.json never leaves the machine", () => { + test("is absent from collectExportFiles even when it exists", async () => { + const { loadMachine } = await lib(); + loadMachine(HOME); + mkdirSync(resolve(HOME, "telos"), { recursive: true }); + writeFileSync(resolve(HOME, "telos", "GOALS.md"), "# Goals\n"); + + const { collectExportFiles } = await import("../src/hooks/lib/export"); + const files = collectExportFiles(); + expect(files.length).toBeGreaterThan(0); + expect(files.some((f) => f.endsWith("machine.json"))).toBe(false); + }); + + test("is denied at the import boundary", async () => { + const { isNeverImport } = await import("../src/hooks/lib/import-merge"); + expect(isNeverImport("machine.json")).toBe(true); + expect(isNeverImport("export-manifest.json")).toBe(true); + }); + + test("registry entries DO travel, so labels can cross", async () => { + const { ensureRegistered } = await lib(); + const m = ensureRegistered(HOME); + const { collectExportFiles } = await import("../src/hooks/lib/export"); + expect(collectExportFiles()).toContain(`memory/machines/${m.id}.md`); + }); +}); + +describe("export manifest", () => { + test("names the producing machine and the file count", async () => { + const { buildManifest } = await import("../src/hooks/lib/export"); + const mf = buildManifest({ id: "id-a", label: "macbook", os: "darwin" }, 7); + expect(mf.machineId).toBe("id-a"); + expect(mf.label).toBe("macbook"); + expect(mf.fileCount).toBe(7); + expect(Number.isNaN(Date.parse(mf.exportedAt))).toBe(false); + }); + + test("readManifest round-trips what buildManifest produced", async () => { + const { buildManifest, MANIFEST_NAME } = await import("../src/hooks/lib/export"); + const { readManifest } = await import("../src/hooks/lib/import-merge"); + const mf = buildManifest({ id: "id-a", label: "macbook", os: "darwin" }, 7); + const got = readManifest([ + { path: MANIFEST_NAME, data: () => Buffer.from(JSON.stringify(mf)) }, + ]); + expect(got?.machineId).toBe("id-a"); + expect(got?.label).toBe("macbook"); + }); + + test("returns null for an archive with no manifest", async () => { + const { readManifest } = await import("../src/hooks/lib/import-merge"); + expect( + readManifest([{ path: "telos/GOALS.md", data: () => Buffer.from("x") }]) + ).toBeNull(); + }); + + test("returns null for a corrupt or id-less manifest", async () => { + const { MANIFEST_NAME } = await import("../src/hooks/lib/export"); + const { readManifest } = await import("../src/hooks/lib/import-merge"); + expect( + readManifest([{ path: MANIFEST_NAME, data: () => Buffer.from("{{{") }]) + ).toBeNull(); + expect( + readManifest([{ path: MANIFEST_NAME, data: () => Buffer.from('{"label":"x"}') }]) + ).toBeNull(); + }); +}); From 15ef621478908bc78f3ac6f1f48b4a703c72b2e8 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 18:07:15 +0200 Subject: [PATCH 30/35] test(machine): close the mutation gaps the ratchet exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stryker left 12 survivors in machine.ts. Each one named a real gap rather than a cosmetic one: - an empty, non-string, or array-shaped id was never exercised, so the usability check could have been reduced to a truthiness test - the repair path never saw a whitespace-only label or an empty os, so the trim and the fallbacks were unasserted - readRegistry was never given a non-markdown file, an entry with an id but no label, or an entry omitting os - displayName was only ever asked for the first registry entry, so matching on id rather than position was unproven machine.ts 87.25% -> 99.02%, export.ts 75% -> 100%. The one remaining survivor is equivalent: dropping the optional chain in `typeof v?.id` throws on a null parse, which the surrounding catch turns into the same regeneration the original produces. There is no observable difference to assert. Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- test/machine.test.ts | 91 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/machine.test.ts b/test/machine.test.ts index c7e402d..705de0b 100644 --- a/test/machine.test.ts +++ b/test/machine.test.ts @@ -85,6 +85,42 @@ describe("loadMachine", () => { }); }); +describe("loadMachine — id validity edge cases", () => { + test("an EMPTY id is not usable and is regenerated", async () => { + const { loadMachine, machineFilePath } = await lib(); + writeFileSync(machineFilePath(HOME), JSON.stringify({ id: "", label: "x" })); + expect(loadMachine(HOME).id.length).toBeGreaterThan(0); + }); + + test("a non-string id is not usable and is regenerated", async () => { + const { loadMachine, machineFilePath } = await lib(); + writeFileSync(machineFilePath(HOME), JSON.stringify({ id: 12345, label: "x" })); + const m = loadMachine(HOME); + expect(typeof m.id).toBe("string"); + expect(m.id).not.toBe("12345"); + }); + + test("an id that is not a string is rejected even when it has a length", async () => { + const { loadMachine, machineFilePath } = await lib(); + writeFileSync(machineFilePath(HOME), JSON.stringify({ id: ["a", "b"], label: "x" })); + const m = loadMachine(HOME); + expect(typeof m.id).toBe("string"); + expect(Array.isArray(m.id)).toBe(false); + }); + + test("a whitespace-only stored label falls back to the default", async () => { + const { loadMachine, machineFilePath, defaultLabel } = await lib(); + writeFileSync(machineFilePath(HOME), JSON.stringify({ id: "kept-id", label: " " })); + expect(loadMachine(HOME).label).toBe(defaultLabel("kept-id")); + }); + + test("a stored os of empty string is replaced with the real platform", async () => { + const { loadMachine, machineFilePath } = await lib(); + writeFileSync(machineFilePath(HOME), JSON.stringify({ id: "kept-id", os: "" })); + expect(loadMachine(HOME).os.length).toBeGreaterThan(0); + }); +}); + describe("defaultLabel", () => { test("is derived from the id, never from the hostname", async () => { const { defaultLabel } = await lib(); @@ -158,6 +194,52 @@ describe("registry", () => { expect(raw).toContain("renamed"); }); + test("ignores non-markdown files in the machines directory", async () => { + const { writeRegistryEntry, readRegistry } = await lib(); + writeRegistryEntry({ id: "id-a", label: "alpha", os: "linux" }); + writeFileSync( + resolve(HOME, "memory", "machines", "notes.txt"), + "id: nope\nlabel: nope\n" + ); + writeFileSync(resolve(HOME, "memory", "machines", "state.json"), '{"id":"x"}'); + const reg = readRegistry(); + expect(reg.length).toBe(1); + expect(reg[0].id).toBe("id-a"); + }); + + test("only .md files count as registry entries, even with valid frontmatter", async () => { + const { writeRegistryEntry, readRegistry } = await lib(); + writeRegistryEntry({ id: "id-a", label: "alpha", os: "linux" }); + writeFileSync( + resolve(HOME, "memory", "machines", "decoy.txt"), + "---\nid: id-decoy\nlabel: phantom\nos: linux\n---\n" + ); + const reg = readRegistry(); + expect(reg.map((e) => e.id)).toEqual(["id-a"]); + }); + + test("drops an entry that has an id but no label", async () => { + const { writeRegistryEntry, readRegistry } = await lib(); + writeRegistryEntry({ id: "id-a", label: "alpha", os: "linux" }); + writeFileSync( + resolve(HOME, "memory", "machines", "half.md"), + "---\nid: id-half\n---\n" + ); + const reg = readRegistry(); + expect(reg.map((e) => e.id)).toEqual(["id-a"]); + }); + + test("defaults os to empty string when the entry omits it", async () => { + const { readRegistry } = await lib(); + mkdirSync(resolve(HOME, "memory", "machines"), { recursive: true }); + writeFileSync( + resolve(HOME, "memory", "machines", "noos.md"), + "---\nid: id-noos\nlabel: nameless\n---\n" + ); + const reg = readRegistry(); + expect(reg.find((e) => e.id === "id-noos")?.os).toBe(""); + }); + test("skips a malformed entry without hiding the rest", async () => { const { writeRegistryEntry, readRegistry } = await lib(); writeRegistryEntry({ id: "id-a", label: "alpha", os: "linux" }); @@ -198,6 +280,15 @@ describe("displayName", () => { expect(displayName("bbbb2222-0000-0000-0000-000000000000", reg)).toBe("macbook·bbbb"); }); + test("matches on id, not on position in the registry", async () => { + const { displayName } = await lib(); + const reg = [ + { id: "id-a", label: "alpha", os: "linux" }, + { id: "id-b", label: "beta", os: "darwin" }, + ]; + expect(displayName("id-b", reg)).toBe("beta"); + }); + test("falls back to the short id when the machine is unknown", async () => { const { displayName } = await lib(); expect(displayName("ffff9999-0000-0000-0000-000000000000", [])).toBe("ffff"); From f270aaad3855fe3798dce5fa66fe1ac9ea95a3a9 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 19:43:27 +0200 Subject: [PATCH 31/35] feat(anchor): resolve project-relative paths instead of raw absolute ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two machines with the same project checked out at different absolute paths could never benefit from retrieval.ts's cwd scope boost: a reflection stamped one machine's absolute path can never string-equal another machine's mount, so SCOPE_BOOST silently never fired across machines. Relocating a project on one machine (project.ts set-path) also orphaned every existing memory that had stamped the old absolute path, since retrieval compared against that stamp directly rather than through the registry. An anchor replaces the absolute prefix with the project's registry slug — `{proj:slug}/relative/path` instead of the machine-specific path — and resolves back to a real path at read time against whatever is registered locally. Same trick as machine.ts's id/label split, applied to paths: store a stable reference, resolve it locally, so a relocation or a second machine is a registry lookup rather than a rewrite of every record that mentions the old path. algorithm-reflect.ts and thread.ts anchor their cwd stamp at write time. retrieval.ts resolves an anchored (or plain, pre-existing) cwd against the local project registry before the scope-boost comparison, loading the registry once per rank() call rather than once per doc. No backfill: existing raw-path records pass through resolveAnchor unchanged and are compared exactly as before. Verified with break-tests (longest-match guard, ignoring the relative segment, treating unresolvable as a match) and Stryker's diff-scoped mutation gate: anchor.ts at 100% excluding two documented-equivalent mutants on a guard that is unreachable under resolveProjectFromCwd's current contract. Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/hooks/lib/anchor.ts | 81 +++++++++++++ src/hooks/lib/retrieval.ts | 10 +- src/tools/agent/algorithm-reflect.ts | 4 +- src/tools/agent/thread.ts | 3 +- test/anchor.test.ts | 171 +++++++++++++++++++++++++++ test/retrieval.test.ts | 67 +++++++++++ 6 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 src/hooks/lib/anchor.ts create mode 100644 test/anchor.test.ts diff --git a/src/hooks/lib/anchor.ts b/src/hooks/lib/anchor.ts new file mode 100644 index 0000000..d22544c --- /dev/null +++ b/src/hooks/lib/anchor.ts @@ -0,0 +1,81 @@ +/** + * Path anchors — `{proj:slug}/relative` instead of an absolute path. + * + * An absolute cwd stamp never matches across machines: same project, + * different mount, different username, different OS. An anchor replaces the + * absolute prefix with the project's registry slug, so resolution happens + * locally at read time — the same trick machine.ts uses for labels, applied + * to paths. Relocating a project (`project.ts set-path`) then fixes every + * memory that ever referenced it, because none of them stored the path. + */ + +import { relative, resolve, sep } from "node:path"; +import type { ProjectProgress } from "./projects"; +import { readAllProjects, resolveProjectFromCwd } from "./projects"; + +const ANCHOR_RE = /^\{proj:([a-z0-9_-]+)\}(\/.*)?$/; + +export function isAnchor(value: string): boolean { + return ANCHOR_RE.test(value); +} + +/** + * Absolute path → `{proj:slug}/relative`, if it falls inside a registered + * project. A path outside every registered project passes through + * unchanged — most cwd stamps from ad hoc commands will never resolve to a + * project, and that is fine; they simply do not benefit yet. + */ +export function encodeAnchor( + absPath: string, + projects: ProjectProgress[] = readAllProjects() +): string { + const proj = resolveProjectFromCwd(absPath, projects); + if (!proj) return absPath; + + // Defensive: resolveProjectFromCwd already guarantees absPath sits inside + // proj.path, so this is unreachable today. Kept as a guard against that + // contract changing rather than as covered behavior. + const rel = relative(resolve(proj.path), resolve(absPath)); + if (rel.startsWith("..")) return absPath; + + const relPosix = rel.split(sep).join("/"); + return relPosix ? `{proj:${proj.name}}/${relPosix}` : `{proj:${proj.name}}`; +} + +export type AnchorResolution = + | { state: "anchored"; path: string } + | { state: "plain"; path: string } + | { state: "unresolvable"; slug: string }; + +/** + * `{proj:slug}/relative` → absolute path on THIS machine, via the local + * registry. A plain (non-anchor) value is returned as-is — either an + * already-local absolute path, or a record captured before this feature + * shipped. There is no backfill, so pre-anchor records are handled exactly + * as they were before. + */ +export function resolveAnchor( + value: string, + projects: ProjectProgress[] = readAllProjects() +): AnchorResolution { + const match = ANCHOR_RE.exec(value); + if (!match) return { state: "plain", path: value }; + + const [, slug, rel] = match; + const proj = projects.find((p) => p.name === slug); + if (!proj) return { state: "unresolvable", slug }; + + const path = rel ? resolve(proj.path, `.${rel}`) : resolve(proj.path); + return { state: "anchored", path }; +} + +/** Does `value` (anchored or plain) refer to `cwd` on this machine? */ +export function anchorMatchesCwd( + value: string, + cwd: string, + projects: ProjectProgress[] = readAllProjects() +): boolean { + const resolved = resolveAnchor(value, projects); + if (resolved.state === "unresolvable") return false; + return resolve(resolved.path) === resolve(cwd); +} diff --git a/src/hooks/lib/retrieval.ts b/src/hooks/lib/retrieval.ts index d7ff34b..7924231 100644 --- a/src/hooks/lib/retrieval.ts +++ b/src/hooks/lib/retrieval.ts @@ -7,6 +7,8 @@ */ import { basename } from "node:path"; +import { anchorMatchesCwd } from "./anchor"; +import { readAllProjects } from "./projects"; import type { IndexedDoc, RetrievalIndex } from "./retrieval-index"; import { extractKeywords } from "./text-similarity"; @@ -87,14 +89,18 @@ function rank(query: string, index: RetrievalIndex, cwd: string): ScoredDoc[] { .toLowerCase() .replace(/[^a-z0-9-]/g, ""); const scopeTokens = scopeKey ? extractKeywords(scopeKey) : new Set<string>(); + // Loaded once per rank() call, not per doc — the registry rarely changes + // within a single retrieval pass. + const projects = readAllProjects(); const scored: ScoredDoc[] = []; for (const doc of index.docs) { const raw = scoreDoc(queryTerms, doc, index.df, N); if (raw === 0) continue; - // Exact cwd match when available; fingerprint heuristic for older captures. + // Anchored or plain cwd resolved against the local registry when + // available; fingerprint heuristic for captures with no cwd at all. const scopeMatch = doc.cwd - ? doc.cwd === cwd + ? anchorMatchesCwd(doc.cwd, cwd, projects) : [...scopeTokens].some((t) => scopeMatches(doc, t)); const boosted = raw * (scopeMatch ? SCOPE_BOOST : 1) * ageDecay(doc.ts); const confidence = boosted / self; diff --git a/src/tools/agent/algorithm-reflect.ts b/src/tools/agent/algorithm-reflect.ts index ede7603..fcc77c9 100644 --- a/src/tools/agent/algorithm-reflect.ts +++ b/src/tools/agent/algorithm-reflect.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun + /** * AlgorithmReflect — Append structured algorithm reflections to JSONL. * @@ -14,6 +15,7 @@ import { appendFileSync } from "node:fs"; import { parseArgs } from "node:util"; +import { encodeAnchor } from "../../hooks/lib/anchor"; import { paths } from "../../hooks/lib/paths"; import { emit } from "../lib/emit"; @@ -107,7 +109,7 @@ Output: algorithm-reflections.jsonl in memory/learning/reflections/ const reflection: AlgorithmReflection = { timestamp: new Date().toISOString(), - cwd: process.cwd(), + cwd: encodeAnchor(process.cwd()), task: values.task, criteria_count: parseInt(values.criteria || "0", 10), criteria_passed: parseInt(values.passed || "0", 10), diff --git a/src/tools/agent/thread.ts b/src/tools/agent/thread.ts index 1db10e5..4197475 100644 --- a/src/tools/agent/thread.ts +++ b/src/tools/agent/thread.ts @@ -14,6 +14,7 @@ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { parseArgs } from "node:util"; +import { encodeAnchor } from "../../hooks/lib/anchor"; import { ensureDir, paths } from "../../hooks/lib/paths"; import { emit } from "../lib/emit"; @@ -65,7 +66,7 @@ export function writeThreads(threads: Thread[]): void { function addThread(title: string, context: string): Thread { const thread: Thread = { id: generateId(), - cwd: process.cwd(), + cwd: encodeAnchor(process.cwd()), title, context, status: "open", diff --git a/test/anchor.test.ts b/test/anchor.test.ts new file mode 100644 index 0000000..429aaf7 --- /dev/null +++ b/test/anchor.test.ts @@ -0,0 +1,171 @@ +import { describe, expect, test } from "bun:test"; +import { resolve } from "node:path"; +import type { ProjectProgress } from "../src/hooks/lib/projects"; + +function project(name: string, path: string): ProjectProgress { + return { name, path, status: "active", created: "2026-01-01", updated: "2026-01-01" }; +} + +async function lib() { + return await import("../src/hooks/lib/anchor"); +} + +describe("isAnchor", () => { + test("recognizes the {proj:slug} form", async () => { + const { isAnchor } = await lib(); + expect(isAnchor("{proj:widget}/src/foo.ts")).toBe(true); + expect(isAnchor("{proj:widget}")).toBe(true); + }); + + test("rejects a plain absolute path", async () => { + const { isAnchor } = await lib(); + expect(isAnchor("/home/dev/git/widget/src/foo.ts")).toBe(false); + }); + + test("rejects a slug with characters outside [a-z0-9_-]", async () => { + const { isAnchor } = await lib(); + expect(isAnchor("{proj:Letter Box}/foo.ts")).toBe(false); + }); + + test("rejects an unclosed or malformed brace", async () => { + const { isAnchor } = await lib(); + expect(isAnchor("{proj:widget/foo.ts")).toBe(false); + expect(isAnchor("proj:widget}/foo.ts")).toBe(false); + }); + + test("requires the pattern to consume the whole string, not just a prefix", async () => { + const { isAnchor } = await lib(); + expect(isAnchor("{proj:widget}garbage")).toBe(false); + expect(isAnchor("{proj:widget}/src.tsextra")).toBe(true); // legitimate: /src.tsextra is a valid relative segment + }); +}); + +describe("encodeAnchor", () => { + const projects = [ + project("widget", "/home/dev/git/widget"), + project("widget-docs", "/home/dev/git/widget-docs"), + ]; + + test("rewrites a path inside a registered project", async () => { + const { encodeAnchor } = await lib(); + expect(encodeAnchor("/home/dev/git/widget/src/foo.ts", projects)).toBe( + "{proj:widget}/src/foo.ts" + ); + }); + + test("the project root itself has no trailing slash", async () => { + const { encodeAnchor } = await lib(); + expect(encodeAnchor("/home/dev/git/widget", projects)).toBe("{proj:widget}"); + }); + + test("passes through a path outside every registered project", async () => { + const { encodeAnchor } = await lib(); + expect(encodeAnchor("/home/dev/scratch/notes", projects)).toBe( + "/home/dev/scratch/notes" + ); + }); + + test("picks the longest matching project when one path prefixes another", async () => { + const { encodeAnchor } = await lib(); + expect(encodeAnchor("/home/dev/git/widget-docs/README.md", projects)).toBe( + "{proj:widget-docs}/README.md" + ); + }); + + test("does not treat a same-prefix sibling directory as a match", async () => { + const { encodeAnchor } = await lib(); + // "widget-docs" must not be matched as if it were inside "widget". + const result = encodeAnchor("/home/dev/git/widget-docs/README.md", [ + project("widget", "/home/dev/git/widget"), + ]); + expect(result).toBe("/home/dev/git/widget-docs/README.md"); + }); +}); + +describe("resolveAnchor", () => { + const projects = [project("widget", "/home/dev/git/widget")]; + + test("resolves a known anchor to a local absolute path", async () => { + const { resolveAnchor } = await lib(); + const r = resolveAnchor("{proj:widget}/src/foo.ts", projects); + expect(r).toEqual({ + state: "anchored", + path: resolve("/home/dev/git/widget/src/foo.ts"), + }); + }); + + test("resolves the bare project anchor to the project root", async () => { + const { resolveAnchor } = await lib(); + const r = resolveAnchor("{proj:widget}", projects); + expect(r).toEqual({ state: "anchored", path: resolve("/home/dev/git/widget") }); + }); + + test("reports unresolvable when the slug is not registered here", async () => { + const { resolveAnchor } = await lib(); + const r = resolveAnchor("{proj:gizmo}/README.md", projects); + expect(r).toEqual({ state: "unresolvable", slug: "gizmo" }); + }); + + test("passes a plain non-anchor value through unchanged", async () => { + const { resolveAnchor } = await lib(); + const r = resolveAnchor("/some/random/path", projects); + expect(r).toEqual({ state: "plain", path: "/some/random/path" }); + }); + + test("round-trips encodeAnchor's output back to the original absolute path", async () => { + const { encodeAnchor, resolveAnchor } = await lib(); + const original = resolve("/home/dev/git/widget/src/deep/nested/foo.ts"); + const encoded = encodeAnchor(original, projects); + const decoded = resolveAnchor(encoded, projects); + expect(decoded).toEqual({ state: "anchored", path: original }); + }); +}); + +describe("anchorMatchesCwd", () => { + const projects = [project("widget", "/home/dev/git/widget")]; + + test("matches when the anchor resolves to the given cwd", async () => { + const { anchorMatchesCwd } = await lib(); + expect( + anchorMatchesCwd("{proj:widget}/src", "/home/dev/git/widget/src", projects) + ).toBe(true); + }); + + test("does not match a different path under the same project", async () => { + const { anchorMatchesCwd } = await lib(); + expect( + anchorMatchesCwd("{proj:widget}/src", "/home/dev/git/widget/test", projects) + ).toBe(false); + }); + + test("an unresolvable anchor never matches, regardless of cwd", async () => { + const { anchorMatchesCwd } = await lib(); + expect(anchorMatchesCwd("{proj:unknown}/src", "/home/dev/git/widget", projects)).toBe( + false + ); + }); + + test("a plain absolute path still matches by direct equality", async () => { + const { anchorMatchesCwd } = await lib(); + expect(anchorMatchesCwd("/home/dev/git/widget", "/home/dev/git/widget", [])).toBe( + true + ); + }); +}); + +describe("relocation — the actual payoff", () => { + test("changing a project's registered path re-resolves every anchor referencing it", async () => { + const { encodeAnchor, resolveAnchor } = await lib(); + const before = [project("widget", "/home/dev/git/widget")]; + const anchor = encodeAnchor("/home/dev/git/widget/src/foo.ts", before); + + // The project moves — same slug, new path. No anchor is rewritten. + const after = [project("widget", "/mnt/data/widget")]; + const resolved = resolveAnchor(anchor, after); + + expect(resolved).toEqual({ + state: "anchored", + path: resolve("/mnt/data/widget/src/foo.ts"), + }); + }); +}); diff --git a/test/retrieval.test.ts b/test/retrieval.test.ts index ec23808..2baaff3 100644 --- a/test/retrieval.test.ts +++ b/test/retrieval.test.ts @@ -259,6 +259,73 @@ describe("retrieval index — dedup against graduated frames", () => { }); }); +describe("retrieval index — anchored cwd (cross-machine scope)", () => { + function fixtureProject(name: string, path: string) { + const dir = resolve(TEST_HOME, "memory", "projects", name); + mkdirSync(dir, { recursive: true }); + writeFileSync( + resolve(dir, "ISA.md"), + `---\nname: "${name}"\npath: "${path}"\nstatus: "active"\ncreated: "2026-01-01"\nupdated: "2026-01-01"\n---\n\n## Goal\n` + ); + } + + test("an anchored reflection scope-matches against the LOCAL registered path, not the path it was captured under", () => { + // Registered here at a path different from wherever it was captured — + // exactly what happens after an import from another machine. + fixtureProject("sample", "/opt/build/sample"); + fixtureReflection({ + timestamp: "2026-05-30T18:00:00Z", + cwd: "{proj:sample}/src", + task: "Build the playwright visual-check skill", + sentiment: 8, + q1: "Verify the external tool's exact CLI contract before hardcoding the screenshot command", + }); + const idx = buildIndex(); + const result = runRetrieval( + "what is the exact CLI contract for this screenshot binary?", + idx, + "/opt/build/sample/src" + ); + expect(result.matches[0].scopeMatch).toBe(true); + }); + + test("does not scope-match a different local cwd under the same project", () => { + fixtureProject("sample", "/opt/build/sample"); + fixtureReflection({ + timestamp: "2026-05-30T18:00:00Z", + cwd: "{proj:sample}/src", + task: "Build the playwright visual-check skill", + sentiment: 8, + q1: "Verify the external tool's exact CLI contract before hardcoding the screenshot command", + }); + const idx = buildIndex(); + const result = runRetrieval( + "what is the exact CLI contract for this screenshot binary?", + idx, + "/opt/build/sample/test" + ); + expect(result.matches[0]?.scopeMatch ?? false).toBe(false); + }); + + test("an anchor whose project is not registered here never scope-matches", () => { + // No fixtureProject call — the slug is unknown on this "machine". + fixtureReflection({ + timestamp: "2026-05-30T18:00:00Z", + cwd: "{proj:gizmo}/src", + task: "Build the playwright visual-check skill", + sentiment: 8, + q1: "Verify the external tool's exact CLI contract before hardcoding the screenshot command", + }); + const idx = buildIndex(); + const result = runRetrieval( + "what is the exact CLI contract for this screenshot binary?", + idx, + "/opt/build/gizmo/src" + ); + expect(result.matches[0]?.scopeMatch ?? false).toBe(false); + }); +}); + describe("retrieval index — reflections", () => { test("indexes reflections with q1 as the displayed principle", () => { fixtureReflection({ From 7b64e6aea374a346047bedeecdf38218b22ce078 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 19:49:01 +0200 Subject: [PATCH 32/35] feat(signals): stamp every signal with its emitting machine's id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emitSignal is the single chokepoint for ratings.jsonl, token-usage.jsonl, and every future signal type, so this covers all of them in one edit. Without an origin, a synced signal from another machine is indistinguishable from a local one — there is no way to scope stats, exclude a machine, or attribute a rating to where it happened. The id comes from machine.ts (already built): a stable uuid, never the label, so renaming a machine never touches a stored signal. Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/hooks/lib/signals.ts | 3 ++- test/signals.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/hooks/lib/signals.ts b/src/hooks/lib/signals.ts index 32fa9d8..44e7e7f 100644 --- a/src/hooks/lib/signals.ts +++ b/src/hooks/lib/signals.ts @@ -1,5 +1,6 @@ import { appendFileSync } from "node:fs"; import { resolve } from "node:path"; +import { loadMachine } from "./machine"; import { paths } from "./paths"; import { now } from "./time"; @@ -14,7 +15,7 @@ function emitSignal( filename: string, data: { type: string; [key: string]: unknown } ): void { - const signal: Signal = { ts: now(), ...data }; + const signal: Signal = { ts: now(), m: loadMachine().id, ...data }; const filepath = resolve(paths.signals(), filename); appendFileSync(filepath, `${JSON.stringify(signal)}\n`); } diff --git a/test/signals.test.ts b/test/signals.test.ts index 8051dec..e02fe5f 100644 --- a/test/signals.test.ts +++ b/test/signals.test.ts @@ -50,3 +50,29 @@ describe("emitRating", () => { expect(last.response_preview).toBeUndefined(); }); }); + +describe("emitSignal — origin stamp", () => { + test("stamps the emitting machine's id on every signal", () => { + emitRating(8, "context", "explicit"); + + const logPath = resolve(TEST_HOME, "memory", "signals", "ratings.jsonl"); + const entry = JSON.parse(readFileSync(logPath, "utf-8").trim()); + const machineFile = JSON.parse( + readFileSync(resolve(TEST_HOME, "machine.json"), "utf-8") + ); + expect(entry.m).toBe(machineFile.id); + }); + + test("stamps the SAME id across multiple signals in one session", () => { + emitRating(5, "first", "explicit"); + emitRating(6, "second", "explicit"); + + const logPath = resolve(TEST_HOME, "memory", "signals", "ratings.jsonl"); + const lines = readFileSync(logPath, "utf-8") + .trim() + .split("\n") + .map((l) => JSON.parse(l)); + expect(lines[0].m).toBe(lines[1].m); + expect(lines[0].m.length).toBeGreaterThan(0); + }); +}); From c3762d85daf31cc9b95bd1fac69f8c3be6999f86 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 19:50:23 +0200 Subject: [PATCH 33/35] feat(relationship): anchor the session comment's cwd stamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The <!-- session:… cwd:… --> comment in daily relationship notes was the last cwd-stamping writer still storing a raw absolute path. Anchored it the same way as the reflection and thread writers. Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/hooks/lib/relationship.ts | 4 +++- test/relationship.test.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/hooks/lib/relationship.ts b/src/hooks/lib/relationship.ts index 239db06..fd0e347 100644 --- a/src/hooks/lib/relationship.ts +++ b/src/hooks/lib/relationship.ts @@ -13,6 +13,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; +import { encodeAnchor } from "./anchor"; import { ensureDir, paths } from "./paths"; type NoteType = "W" | "O" | "Session"; @@ -65,7 +66,8 @@ export function appendNotes(notes: RelationshipNote[], sessionId?: string): void const timestamp = new Date().toTimeString().slice(0, 5); lines.push(`## ${timestamp}`); - if (sessionId) lines.push(`<!-- session:${sessionId} cwd:${process.cwd()} -->`); + if (sessionId) + lines.push(`<!-- session:${sessionId} cwd:${encodeAnchor(process.cwd())} -->`); for (const note of fresh) { if (note.type === "O" && note.confidence !== undefined) { diff --git a/test/relationship.test.ts b/test/relationship.test.ts index 86e7208..e8562f7 100644 --- a/test/relationship.test.ts +++ b/test/relationship.test.ts @@ -105,6 +105,22 @@ describe("appendNotes", () => { expect(readFileSync(todayFile(), "utf-8")).not.toContain("<!-- session:"); }); + test("anchors the cwd to {proj:slug} when it falls inside a registered project", () => { + const slug = "test-repo"; + const dir = resolve(HOME, "memory", "projects", slug); + mkdirSync(dir, { recursive: true }); + writeFileSync( + resolve(dir, "ISA.md"), + `---\nname: "${slug}"\npath: "${process.cwd()}"\nstatus: "active"\ncreated: "2026-01-01"\nupdated: "2026-01-01"\n---\n\n## Goal\n` + ); + + appendNotes([{ type: "W", text: "a fact" }], "sess-456"); + + const content = readFileSync(todayFile(), "utf-8"); + expect(content).toContain(`cwd:{proj:${slug}}`); + expect(content).not.toContain(`cwd:${process.cwd()}`); + }); + test("skips a note whose text is already present", () => { appendNotes([{ type: "W", text: "duplicated fact" }]); appendNotes([{ type: "W", text: "duplicated fact" }]); From 45dc414229c04bed133836c3af5b76b467e24501 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 19:52:30 +0200 Subject: [PATCH 34/35] feat(algorithm-reflect): stamp cwd anchor and machine id on every reflection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted the record-assembly logic out of the argv-parsing CLI entrypoint into an exported buildReflection(), so the anchor and origin stamping are directly unit-testable without spawning the CLI — Stryker's diff gate excludes subprocess-driven suites from mutation scoring, so this is what it takes for the wiring to actually be graded. Same two stamps as the other writers: cwd anchored via encodeAnchor, and m set to this machine's id via machine.ts. No behavior change to the CLI itself. Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/tools/agent/algorithm-reflect.ts | 54 +++++++++++++++----- test/algorithm-reflect-build.test.ts | 74 ++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 test/algorithm-reflect-build.test.ts diff --git a/src/tools/agent/algorithm-reflect.ts b/src/tools/agent/algorithm-reflect.ts index fcc77c9..cfe69cd 100644 --- a/src/tools/agent/algorithm-reflect.ts +++ b/src/tools/agent/algorithm-reflect.ts @@ -16,6 +16,7 @@ import { appendFileSync } from "node:fs"; import { parseArgs } from "node:util"; import { encodeAnchor } from "../../hooks/lib/anchor"; +import { loadMachine } from "../../hooks/lib/machine"; import { paths } from "../../hooks/lib/paths"; import { emit } from "../lib/emit"; @@ -24,6 +25,7 @@ import { emit } from "../lib/emit"; interface AlgorithmReflection { timestamp: string; cwd: string; + m: string; task: string; criteria_count: number; criteria_passed: number; @@ -43,6 +45,40 @@ function reflectionsPath(): string { return paths.reflectionsFile(); } +/** + * Assemble a reflection record from CLI-style input, stamping the current + * cwd (anchored) and this machine's id. Exported so the stamping logic is + * directly testable without going through argv parsing. + */ +export function buildReflection(input: { + task: string; + q1: string; + q2: string; + q3: string; + criteria_count?: number; + criteria_passed?: number; + criteria_failed?: number; + sentiment?: number; + scope?: string; +}): AlgorithmReflection { + return { + timestamp: new Date().toISOString(), + cwd: encodeAnchor(process.cwd()), + m: loadMachine().id, + task: input.task, + criteria_count: input.criteria_count ?? 0, + criteria_passed: input.criteria_passed ?? 0, + criteria_failed: input.criteria_failed ?? 0, + sentiment: Math.max(1, Math.min(10, input.sentiment ?? 5)), + q1: input.q1, + q2: input.q2, + q3: input.q3, + // Default to general (the ~94% case); only "task-specific" suppresses it + // from algorithm-update clustering. + scope: input.scope === "task-specific" ? "task-specific" : "general", + }; +} + function appendReflection(reflection: AlgorithmReflection): { success: boolean; message: string; @@ -107,21 +143,17 @@ Output: algorithm-reflections.jsonl in memory/learning/reflections/ process.exit(1); } - const reflection: AlgorithmReflection = { - timestamp: new Date().toISOString(), - cwd: encodeAnchor(process.cwd()), + const reflection = buildReflection({ task: values.task, - criteria_count: parseInt(values.criteria || "0", 10), - criteria_passed: parseInt(values.passed || "0", 10), - criteria_failed: parseInt(values.failed || "0", 10), - sentiment: Math.max(1, Math.min(10, parseInt(values.sentiment || "5", 10))), q1: values.q1, q2: values.q2, q3: values.q3, - // Default to general (the ~94% case); only "task-specific" suppresses it - // from algorithm-update clustering. - scope: values.scope === "task-specific" ? "task-specific" : "general", - }; + criteria_count: parseInt(values.criteria || "0", 10), + criteria_passed: parseInt(values.passed || "0", 10), + criteria_failed: parseInt(values.failed || "0", 10), + sentiment: parseInt(values.sentiment || "5", 10), + scope: values.scope, + }); const result = appendReflection(reflection); emit.ok(result.message); diff --git a/test/algorithm-reflect-build.test.ts b/test/algorithm-reflect-build.test.ts new file mode 100644 index 0000000..f326f0a --- /dev/null +++ b/test/algorithm-reflect-build.test.ts @@ -0,0 +1,74 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const HOME = resolve(import.meta.dir, "../.test-home-algorithm-reflect-build"); + +beforeEach(() => { + process.env.PAL_HOME = HOME; + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); +}); + +afterEach(() => { + delete process.env.PAL_HOME; + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); +}); + +async function lib() { + return await import("../src/tools/agent/algorithm-reflect"); +} + +const MIN_INPUT = { task: "t", q1: "a", q2: "b", q3: "c" }; + +describe("buildReflection", () => { + test("stamps this machine's id", async () => { + const { buildReflection } = await lib(); + const r = buildReflection(MIN_INPUT); + const machineFile = JSON.parse(readFileSync(resolve(HOME, "machine.json"), "utf-8")); + expect(r.m).toBe(machineFile.id); + }); + + test("anchors the cwd when it falls inside a registered project", async () => { + const slug = "test-repo"; + const dir = resolve(HOME, "memory", "projects", slug); + mkdirSync(dir, { recursive: true }); + writeFileSync( + resolve(dir, "ISA.md"), + `---\nname: "${slug}"\npath: "${process.cwd()}"\nstatus: "active"\ncreated: "2026-01-01"\nupdated: "2026-01-01"\n---\n\n## Goal\n` + ); + const { buildReflection } = await lib(); + const r = buildReflection(MIN_INPUT); + expect(r.cwd).toBe(`{proj:${slug}}`); + }); + + test("passes the cwd through unchanged when no project is registered", async () => { + const { buildReflection } = await lib(); + const r = buildReflection(MIN_INPUT); + expect(r.cwd).toBe(process.cwd()); + }); + + test("clamps sentiment into 1..10", async () => { + const { buildReflection } = await lib(); + expect(buildReflection({ ...MIN_INPUT, sentiment: 99 }).sentiment).toBe(10); + expect(buildReflection({ ...MIN_INPUT, sentiment: -5 }).sentiment).toBe(1); + expect(buildReflection(MIN_INPUT).sentiment).toBe(5); + }); + + test("defaults scope to general unless task-specific is given", async () => { + const { buildReflection } = await lib(); + expect(buildReflection(MIN_INPUT).scope).toBe("general"); + expect(buildReflection({ ...MIN_INPUT, scope: "task-specific" }).scope).toBe( + "task-specific" + ); + expect(buildReflection({ ...MIN_INPUT, scope: "bogus" }).scope).toBe("general"); + }); + + test("defaults counts to zero when omitted", async () => { + const { buildReflection } = await lib(); + const r = buildReflection(MIN_INPUT); + expect(r.criteria_count).toBe(0); + expect(r.criteria_passed).toBe(0); + expect(r.criteria_failed).toBe(0); + }); +}); From 3aef6de2dbe9ccdb2cc6213a647385370c05f859 Mon Sep 17 00:00:00 2001 From: Richard Kovacs <richardkovacs.it@gmail.com> Date: Tue, 18 Aug 2026 19:53:54 +0200 Subject: [PATCH 35/35] feat(thread): stamp cwd anchor and machine id on every thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exported addThread() so the anchor and origin stamping are directly unit-testable without spawning the CLI. Same two stamps as the other writers: cwd anchored via encodeAnchor, m set to this machine's id. That closes out Phase 1's writer set: every cwd-stamping record (reflections, threads, relationship notes) and the central signals chokepoint now carry a portable anchor and a machine origin. Co-authored by Jarvis · https://github.com/kovrichard/portable-agent-layer --- src/tools/agent/thread.ts | 6 +++- test/thread-build.test.ts | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 test/thread-build.test.ts diff --git a/src/tools/agent/thread.ts b/src/tools/agent/thread.ts index 4197475..d645107 100644 --- a/src/tools/agent/thread.ts +++ b/src/tools/agent/thread.ts @@ -15,6 +15,7 @@ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs import { resolve } from "node:path"; import { parseArgs } from "node:util"; import { encodeAnchor } from "../../hooks/lib/anchor"; +import { loadMachine } from "../../hooks/lib/machine"; import { ensureDir, paths } from "../../hooks/lib/paths"; import { emit } from "../lib/emit"; @@ -23,6 +24,7 @@ import { emit } from "../lib/emit"; export interface Thread { id: string; cwd: string; + m: string; title: string; context: string; status: "open" | "resolved"; @@ -63,10 +65,12 @@ export function writeThreads(threads: Thread[]): void { // ── Operations ── -function addThread(title: string, context: string): Thread { +/** Exported so the cwd-anchor and origin-stamp wiring is directly testable. */ +export function addThread(title: string, context: string): Thread { const thread: Thread = { id: generateId(), cwd: encodeAnchor(process.cwd()), + m: loadMachine().id, title, context, status: "open", diff --git a/test/thread-build.test.ts b/test/thread-build.test.ts new file mode 100644 index 0000000..d8f1c9d --- /dev/null +++ b/test/thread-build.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const HOME = resolve(import.meta.dir, "../.test-home-thread-build"); + +beforeEach(() => { + process.env.PAL_HOME = HOME; + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); + mkdirSync(HOME, { recursive: true }); +}); + +afterEach(() => { + delete process.env.PAL_HOME; + if (existsSync(HOME)) rmSync(HOME, { recursive: true }); +}); + +async function lib() { + return await import("../src/tools/agent/thread"); +} + +describe("addThread", () => { + test("stamps this machine's id", async () => { + const { addThread } = await lib(); + const t = addThread("a title", "some context"); + const machineFile = JSON.parse(readFileSync(resolve(HOME, "machine.json"), "utf-8")); + expect(t.m).toBe(machineFile.id); + }); + + test("anchors the cwd when it falls inside a registered project", async () => { + const slug = "test-repo"; + const dir = resolve(HOME, "memory", "projects", slug); + mkdirSync(dir, { recursive: true }); + writeFileSync( + resolve(dir, "ISA.md"), + `---\nname: "${slug}"\npath: "${process.cwd()}"\nstatus: "active"\ncreated: "2026-01-01"\nupdated: "2026-01-01"\n---\n\n## Goal\n` + ); + const { addThread } = await lib(); + const t = addThread("a title", "some context"); + expect(t.cwd).toBe(`{proj:${slug}}`); + }); + + test("passes the cwd through unchanged when no project is registered", async () => { + const { addThread } = await lib(); + const t = addThread("a title", "some context"); + expect(t.cwd).toBe(process.cwd()); + }); + + test("persists the stamped thread to threads.jsonl", async () => { + const { addThread } = await lib(); + const t = addThread("a title", "some context"); + const lines = readFileSync(resolve(HOME, "memory", "state", "threads.jsonl"), "utf-8") + .trim() + .split("\n") + .map((l) => JSON.parse(l)); + expect(lines).toHaveLength(1); + expect(lines[0].id).toBe(t.id); + expect(lines[0].m).toBe(t.m); + }); + + test("starts a new thread as open with no resolved timestamp", async () => { + const { addThread } = await lib(); + const t = addThread("a title", "some context"); + expect(t.status).toBe("open"); + expect(t.resolved).toBeNull(); + }); +});