- Help us make 0.3.1 useful.
+ Help us make the next release useful.
Tell us your package version, OS, terminal/font and a small
reproduction. Original workplace-safe character ideas are welcome.
diff --git a/siblings-0.4.0.js b/siblings-0.4.0.js
new file mode 100644
index 0000000..d3480e5
--- /dev/null
+++ b/siblings-0.4.0.js
@@ -0,0 +1,511 @@
+import {
+ errorCatalog,
+ listErrors,
+ translateError,
+ translateErrors,
+ renderTranslation,
+} from "./vendor/error-translator/index.js";
+import {
+ listExcuses,
+ categories,
+ excuse,
+ excuseBatch,
+ excuseReport,
+} from "./vendor/excuse-js/index.js";
+import { barkLines, createDogLogger } from "./vendor/doggo-log/index.js";
+import {
+ releasePlan,
+ releaseScenarios,
+ scenarioEvidence,
+ releaseGate,
+ preflight,
+ shipIt,
+} from "./vendor/ship-it-meter/index.js";
+const el = (id) => document.getElementById(id);
+const q = (value) => "'" + String(value).replaceAll("'", "'\\''") + "'";
+const select = (id, label, values, selected) => {
+ const wrap = document.createElement("label");
+ wrap.textContent = label;
+ const input = document.createElement("select");
+ input.id = id;
+ for (const value of values) {
+ const option = document.createElement("option");
+ option.value = option.textContent = value;
+ option.selected = value === selected;
+ input.append(option);
+ }
+ wrap.append(input);
+ return wrap;
+};
+const field = (id, label, value, type = "text") => {
+ const wrap = document.createElement("label");
+ wrap.textContent = label;
+ const input = document.createElement("input");
+ input.id = id;
+ input.type = type;
+ input.value = value;
+ if (type === "number") input.min = "0";
+ wrap.append(input);
+ return wrap;
+};
+const check = (id, label, value = true) => {
+ const wrap = field(id, label, "", "checkbox");
+ wrap.firstElementChild.checked = value;
+ return wrap;
+};
+function setup(section, button, output, fields, run) {
+ const form = document.createElement("form");
+ form.className = "demo-controls";
+ form.append(...fields);
+ el(button).before(form);
+ form.addEventListener("submit", (event) => event.preventDefault());
+ const caption = document.createElement("p");
+ caption.textContent = "Try it in your terminal (sh/bash):";
+ const command = document.createElement("pre");
+ command.className = "command";
+ command.setAttribute("aria-label", `${section} command`);
+ const copy = document.createElement("button");
+ copy.type = "button";
+ copy.textContent = "Copy command";
+ const status = document.createElement("p");
+ status.setAttribute("role", "status");
+ el(output).after(caption, command, copy, status);
+ copy.addEventListener("click", async () => {
+ try {
+ await navigator.clipboard.writeText(command.textContent);
+ status.textContent = "Command copied.";
+ } catch {
+ status.textContent = "Select and copy the command manually.";
+ }
+ });
+ const render = () => {
+ try {
+ const result = run();
+ caption.textContent =
+ result.caption || "Try it in your terminal (sh/bash):";
+ copy.textContent = result.caption ? "Copy example" : "Copy command";
+ el(output).textContent = result.output;
+ command.textContent = result.command;
+ copy.disabled = false;
+ status.textContent = "";
+ } catch (error) {
+ el(output).textContent = "Input needs attention: " + error.message;
+ command.textContent = "";
+ copy.disabled = true;
+ }
+ };
+ el(button).addEventListener("click", render);
+ form.addEventListener("input", render);
+ render();
+}
+// Catalogs are read from the actual package, so the choices match its capabilities.
+setup(
+ "error-translator",
+ "translate",
+ "translations",
+ [
+ select(
+ "error-code",
+ "Supported code / custom input",
+ [...listErrors(), "Custom message", "JSON batch", "Full catalog"],
+ "ENOENT",
+ ),
+ select(
+ "error-mode",
+ "Explanation style",
+ ["plain", "rubber-duck"],
+ "rubber-duck",
+ ),
+ select("error-format", "Output format", ["readable", "JSON"], "readable"),
+ ],
+ () => {
+ const code = el("error-code").value,
+ batch = code === "JSON batch";
+ el("errors").parentElement.hidden = ![
+ "Custom message",
+ "JSON batch",
+ ].includes(code);
+ el("errors").disabled = !["Custom message", "JSON batch"].includes(code);
+ el("errors").parentElement.firstChild.textContent = batch
+ ? "Error batch (JSON array)"
+ : "Custom error message";
+ const input = batch
+ ? JSON.parse(el("errors").value)
+ : code === "Custom message"
+ ? el("errors").value
+ : code;
+ const options = { mode: el("error-mode").value };
+ const catalog = code === "Full catalog";
+ const result = catalog
+ ? errorCatalog(options)
+ : batch
+ ? translateErrors(input, options)
+ : translateError(input, options);
+ return {
+ output:
+ el("error-format").value === "JSON"
+ ? JSON.stringify(result, null, 2)
+ : batch || catalog
+ ? result.map(renderTranslation).join("\n\n---\n\n")
+ : renderTranslation(result),
+ command:
+ (batch ? `printf '%s' ${q(el("errors").value)} | ` : "") +
+ `npx @deployanyway/error-translator@0.4.0 ${catalog ? "--catalog" : batch ? "--batch" : q(input)} --mode ${options.mode}` +
+ (el("error-format").value === "JSON" ? " --json" : ""),
+ };
+ },
+);
+for (const category of categories()) {
+ const option = document.createElement("option");
+ option.value = option.textContent = category;
+ el("excuse-category").append(option);
+}
+setup(
+ "excuse-js",
+ "excuse-report",
+ "excuse-output",
+ [
+ select(
+ "excuse-mode",
+ "Generate",
+ ["report", "single", "batch", "catalog"],
+ "report",
+ ),
+ field("excuse-seed", "Seed (blank = random)", "demo"),
+ field("excuse-count", "Batch count (1–20)", "3", "number"),
+ select("excuse-format", "Output format", ["readable", "JSON"], "readable"),
+ ],
+ () => {
+ const category = el("excuse-category").value,
+ mode = el("excuse-mode").value,
+ seed = el("excuse-seed").value,
+ options = seed ? { seed } : {};
+ el("excuse-count").disabled = mode !== "batch";
+ el("excuse-seed").disabled = mode === "catalog";
+ const result =
+ mode === "catalog"
+ ? listExcuses(category)
+ : mode === "report"
+ ? excuseReport(category, options)
+ : mode === "batch"
+ ? excuseBatch(category, {
+ ...options,
+ count: Number(el("excuse-count").value),
+ })
+ : excuse(category, options);
+ return {
+ output:
+ el("excuse-format").value === "JSON"
+ ? JSON.stringify(result, null, 2)
+ : mode === "report"
+ ? result.excuse + "\n\nNext step: " + result.nextStep
+ : Array.isArray(result)
+ ? result.map((item, i) => `${i + 1}. ${item}`).join("\n")
+ : result,
+ command:
+ `npx @deployanyway/excuse-js@0.4.0 ${category}` +
+ (seed && mode !== "catalog" ? ` --seed ${q(seed)}` : "") +
+ (mode === "catalog"
+ ? " --catalog"
+ : mode === "report"
+ ? " --report"
+ : mode === "batch"
+ ? ` --count ${el("excuse-count").value}`
+ : "") +
+ (el("excuse-format").value === "JSON" ? " --json" : ""),
+ };
+ },
+);
+el("excuse-category").addEventListener("change", () =>
+ el("excuse-report").click(),
+);
+setup(
+ "doggo-log",
+ "log-context",
+ "log-output",
+ [
+ select(
+ "dog-demo",
+ "Demonstration",
+ ["one log", "eight-log sequence", "commentary catalog"],
+ "one log",
+ ),
+ select("dog-bark-mode", "Commentary mode", ["classic", "rotate"], "rotate"),
+ field("dog-seed", "Commentary seed", "dallas"),
+ field("dog-message", "Message", "Evidence fetched. Good dog."),
+ select(
+ "dog-method",
+ "Log method",
+ ["log", "debug", "info", "success", "warn", "error"],
+ "info",
+ ),
+ select(
+ "dog-level",
+ "Minimum level",
+ ["debug", "log", "info", "success", "warn", "error"],
+ "debug",
+ ),
+ field("dog-prefix", "Prefix", "api"),
+ field(
+ "dog-context",
+ "Additional context (JSON scalar fields)",
+ '{"environment":"demo"}',
+ ),
+ select("dog-format", "Output format", ["JSON", "text"], "JSON"),
+ check("dog-bark", "Dog commentary"),
+ check("dog-emoji", "Emoji"),
+ check("dog-time", "Timestamp", false),
+ check("dog-quiet", "Quiet (suppress output)", false),
+ ],
+ () => {
+ const rawContext = JSON.parse(el("dog-context").value);
+ if (
+ !rawContext ||
+ typeof rawContext !== "object" ||
+ Array.isArray(rawContext)
+ )
+ throw new TypeError("Context must be a JSON object of scalar fields.");
+ const context = { ...rawContext, requestId: el("request-id").value };
+ const config = {
+ context,
+ prefix: el("dog-prefix").value,
+ level: el("dog-level").value,
+ json: el("dog-format").value === "JSON",
+ bark: el("dog-bark").checked,
+ barkMode: el("dog-bark-mode").value,
+ seed: el("dog-seed").value,
+ emoji: el("dog-emoji").checked,
+ timestamp: el("dog-time").checked,
+ quiet: el("dog-quiet").checked,
+ write: () => {},
+ };
+ const method = el("dog-method").value,
+ demo = el("dog-demo").value;
+ if (demo === "commentary catalog")
+ return {
+ output: barkLines(method)
+ .map((line, i) => i + 1 + ". " + line)
+ .join("\n"),
+ caption:
+ "Install @deployanyway/doggo-log@0.4.0; save as demo.mjs and run node demo.mjs:",
+ command:
+ "import { barkLines } from '@deployanyway/doggo-log';\nconsole.log(barkLines(" +
+ JSON.stringify(method) +
+ ").join('\\n'));",
+ };
+ const logger = createDogLogger(config);
+ if (demo === "eight-log sequence") {
+ const results = Array.from({ length: 8 }, () =>
+ logger[method](el("dog-message").value),
+ );
+ const codeConfig = { ...config };
+ delete codeConfig.write;
+ return {
+ output:
+ results.filter((value) => value !== undefined).join("\n") ||
+ "No logs emitted: quiet mode or minimum level filtered these messages.",
+ caption:
+ "Install @deployanyway/doggo-log@0.4.0; save as demo.mjs and run node demo.mjs:",
+ command:
+ "import { createDogLogger } from '@deployanyway/doggo-log';\nconst log = createDogLogger(" +
+ JSON.stringify(codeConfig, null, 2) +
+ ");\nfor (let i = 0; i < 8; i++) log." +
+ method +
+ "(" +
+ JSON.stringify(el("dog-message").value) +
+ ");",
+ };
+ }
+ const result = logger[method](el("dog-message").value);
+ return {
+ output:
+ result === undefined
+ ? "No log emitted: quiet mode or the minimum level filtered this message."
+ : config.json
+ ? JSON.stringify(JSON.parse(result), null, 2)
+ : result,
+ command:
+ `npx @deployanyway/doggo-log@0.4.0 ${el("dog-method").value} ${q(el("dog-message").value)} --level ${config.level} --prefix ${q(config.prefix)} --context ${q(JSON.stringify(context))}` +
+ (config.json ? " --json" : "") +
+ (config.bark ? " --bark" : "") +
+ ` --bark-mode ${config.barkMode} --seed ${q(config.seed)}` +
+ (!config.emoji ? " --no-emoji" : "") +
+ (config.timestamp ? " --timestamp" : "") +
+ (config.quiet ? " --quiet" : ""),
+ };
+ },
+);
+el("request-id").addEventListener("input", () => el("log-context").click());
+const scenarioWrap = select(
+ "ship-scenario",
+ "Example evidence scenario",
+ releaseScenarios().map((item) => item.name),
+ "ready",
+);
+const scenarioButton = document.createElement("button");
+scenarioButton.type = "button";
+scenarioButton.textContent = "Load scenario";
+const scenarioDescription = document.createElement("p");
+scenarioDescription.setAttribute("role", "status");
+el("evidence").parentElement.before(
+ scenarioWrap,
+ scenarioButton,
+ scenarioDescription,
+);
+scenarioWrap.addEventListener("change", () => {
+ scenarioDescription.textContent = releaseScenarios().find(
+ (item) => item.name === el("ship-scenario").value,
+ ).description;
+});
+scenarioButton.addEventListener("click", () => {
+ const data = scenarioEvidence(el("ship-scenario").value);
+ el("evidence").value = JSON.stringify(data, null, 2);
+ for (const [id, key] of [
+ ["ship-tests", "tests"],
+ ["ship-failing", "failingTests"],
+ ["ship-coverage", "coverage"],
+ ["ship-critical", "criticalIssues"],
+ ["ship-lint", "lintFailures"],
+ ["ship-branch", "branch"],
+ ])
+ el(id).value = data[key] ?? "";
+ el("ship-build").value =
+ data.build === undefined ? "not provided" : data.build ? "pass" : "fail";
+ el("ship-day").value = data.day ?? "monday";
+ el("ship-dirty").checked = data.uncommittedChanges ?? false;
+ scenarioDescription.textContent = releaseScenarios().find(
+ (item) => item.name === el("ship-scenario").value,
+ ).description;
+ el("gate").click();
+});
+const evidenceForm = document.createElement("form");
+evidenceForm.className = "demo-controls";
+const evidenceFields = [
+ field("ship-tests", "Total tests", "42", "number"),
+ field("ship-failing", "Failing tests", "0", "number"),
+ field("ship-coverage", "Coverage %", "92", "number"),
+ select("ship-build", "Build", ["pass", "fail", "not provided"], "pass"),
+ field("ship-critical", "Critical issues", "0", "number"),
+ field("ship-lint", "Lint failures", "0", "number"),
+ field("ship-branch", "Branch", "main"),
+ select(
+ "ship-day",
+ "Weekday",
+ [
+ "monday",
+ "tuesday",
+ "wednesday",
+ "thursday",
+ "friday",
+ "saturday",
+ "sunday",
+ ],
+ "monday",
+ ),
+ check("ship-dirty", "Uncommitted changes", false),
+];
+evidenceForm.append(...evidenceFields);
+el("evidence").parentElement.before(evidenceForm);
+evidenceForm.addEventListener("submit", (event) => event.preventDefault());
+evidenceForm.addEventListener("input", () => {
+ const data = {
+ tests: Number(el("ship-tests").value),
+ failingTests: Number(el("ship-failing").value),
+ coverage: Number(el("ship-coverage").value),
+ criticalIssues: Number(el("ship-critical").value),
+ lintFailures: Number(el("ship-lint").value),
+ branch: el("ship-branch").value,
+ day: el("ship-day").value,
+ uncommittedChanges: el("ship-dirty").checked,
+ };
+ for (const [id, key] of [
+ ["ship-tests", "tests"],
+ ["ship-failing", "failingTests"],
+ ["ship-coverage", "coverage"],
+ ["ship-critical", "criticalIssues"],
+ ["ship-lint", "lintFailures"],
+ ["ship-branch", "branch"],
+ ]) {
+ if (el(id).value === "") delete data[key];
+ }
+ if (el("ship-build").value !== "not provided")
+ data.build = el("ship-build").value === "pass";
+ el("evidence").value = JSON.stringify(data, null, 2);
+ el("gate").click();
+});
+setup(
+ "ship-it-meter",
+ "gate",
+ "gate-output",
+ [
+ select(
+ "ship-mode",
+ "Evaluation",
+ ["release plan", "release gate", "preflight checklist", "score"],
+ "release plan",
+ ),
+ field("ship-min", "Gate minimum score (0–100)", "80", "number"),
+ select("ship-format", "Output format", ["readable", "JSON"], "readable"),
+ ],
+ () => {
+ const input = JSON.parse(el("evidence").value),
+ mode = el("ship-mode").value;
+ el("ship-min").disabled = !["release gate", "release plan"].includes(mode);
+ const result =
+ mode === "release plan"
+ ? releasePlan(input, { minScore: Number(el("ship-min").value) })
+ : mode === "release gate"
+ ? releaseGate(input, { minScore: Number(el("ship-min").value) })
+ : mode === "preflight checklist"
+ ? preflight(input)
+ : shipIt(input);
+ const text = [
+ result.summary,
+ `Score: ${result.score}/100 — ${result.verdict}`,
+ result.passed === undefined
+ ? ""
+ : `Gate: ${result.passed ? "PASS" : "BLOCKED"}`,
+ ...(result.tasks
+ ? [
+ "\nRELEASE TASKS",
+ ...result.tasks.map(
+ (task) =>
+ `[${task.priority}] ${task.title}\nVerify: ${task.verify}`,
+ ),
+ ]
+ : []),
+ ...["reasons", "blockers", "actions"]
+ .filter((key) => result[key]?.length)
+ .map(
+ (key) =>
+ `\n${key.toUpperCase()}\n` +
+ result[key].map((item) => "- " + item).join("\n"),
+ ),
+ ]
+ .filter(Boolean)
+ .join("\n");
+ return {
+ output:
+ el("ship-format").value === "JSON"
+ ? JSON.stringify(result, null, 2)
+ : text,
+ command:
+ `printf '%s' ${q(el("evidence").value)} | npx @deployanyway/ship-it-meter@0.4.0 --stdin` +
+ (mode === "release plan"
+ ? ` --plan --min-score ${el("ship-min").value}`
+ : mode === "release gate"
+ ? ` --gate --min-score ${el("ship-min").value}`
+ : mode === "preflight checklist"
+ ? " --checklist"
+ : "") +
+ (el("ship-format").value === "JSON" ? " --json" : ""),
+ };
+ },
+);
+el("evidence").addEventListener("input", () => el("gate").click());
+
+// Raw evidence stays editable for missing fields and API validation examples.
+el("evidence").parentElement.firstChild.textContent =
+ "Evidence JSON (generated by controls; also directly editable)";
+
+el("errors").addEventListener("input", () => el("translate").click());
diff --git a/style-54d303f2.css b/style-54d303f2.css
index 233f8dc..6d7cdf6 100644
--- a/style-54d303f2.css
+++ b/style-54d303f2.css
@@ -84,6 +84,16 @@ pre {
margin: 0;
white-space: pre;
}
+.preset-library {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ padding: 16px;
+ color: #f4eee3;
+}
+.tool-section > .terminal {
+ max-height: 36rem;
+ overflow: auto;
+}
#status {
min-height: 1.5em;
color: #7e321e;
diff --git a/test/candidate.test.js b/test/candidate.test.js
index abc99ab..d2314cb 100644
--- a/test/candidate.test.js
+++ b/test/candidate.test.js
@@ -1,14 +1,32 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
-import { renderBro, listCharacters, moods } from "../dist/browser.js";
-import { translateErrors } from "../vendor/error-translator/index.js";
-import { excuseReport } from "../vendor/excuse-js/index.js";
-import { createDogLogger } from "../vendor/doggo-log/index.js";
-import { releaseGate } from "../vendor/ship-it-meter/index.js";
-test("candidate demo uses actual flagship and sibling APIs", () => {
+import {
+ renderBro,
+ listCharacters,
+ moods,
+ messageCategories,
+ messagePresets,
+} from "../dist/browser.js";
+import {
+ translateErrors,
+ errorCatalog,
+} from "../vendor/error-translator/index.js";
+import {
+ excuseReport,
+ categories,
+ listExcuses,
+} from "../vendor/excuse-js/index.js";
+import { createDogLogger, barkLines } from "../vendor/doggo-log/index.js";
+import {
+ releaseGate,
+ releasePlan,
+ releaseScenarios,
+ scenarioEvidence,
+} from "../vendor/ship-it-meter/index.js";
+test("released demo uses actual flagship and sibling APIs", () => {
assert.equal(listCharacters().length, 13);
- assert.equal(moods().length, 12);
+ assert.equal(moods().length, 20);
assert.match(
renderBro({ text: "Hello 👋", mode: "think" }).rendered,
/ o\n/,
@@ -29,11 +47,42 @@ test("candidate demo uses actual flagship and sibling APIs", () => {
true,
);
});
-test("candidate presentation is honest and renders input as text", () => {
+test("vendored released catalogs and plans expose the expanded libraries", () => {
+ assert.equal(messageCategories().flatMap(messagePresets).length, 48);
+ assert.equal(errorCatalog().length, 46);
+ assert.equal(categories().flatMap(listExcuses).length, 132);
+ assert.equal(
+ ["log", "debug", "info", "success", "warn", "error"].flatMap(barkLines)
+ .length,
+ 48,
+ );
+ const log = createDogLogger({
+ json: true,
+ bark: true,
+ barkMode: "rotate",
+ seed: "demo",
+ write: () => {},
+ });
+ assert.equal(
+ new Set(
+ Array.from(
+ { length: 8 },
+ () => JSON.parse(log.info("Same message")).commentary,
+ ),
+ ).size,
+ 8,
+ );
+ assert.equal(releaseScenarios().length, 12);
+ const plan = releasePlan(scenarioEvidence("failed-build"));
+ assert.equal(plan.passed, false);
+ assert.equal(plan.tasks[0].priority, "blocker");
+ assert.ok(plan.tasks.every((task) => task.verify));
+});
+test("released presentation is honest and renders input as text", () => {
const html = readFileSync(new URL("../index.html", import.meta.url), "utf8");
assert.match(html, /AVAILABLE ON NPM/);
assert.match(html, /aria-live="polite"/);
- for (const file of ["app-0.3.0.js", "siblings-0.3.0.js"])
+ for (const file of ["app-0.4.0.js", "siblings-0.4.0.js"])
assert.doesNotMatch(
readFileSync(new URL("../" + file, import.meta.url), "utf8"),
/\.innerHTML\s*=/,
diff --git a/test/site.test.js b/test/site.test.js
index f8b6af9..73a8e26 100644
--- a/test/site.test.js
+++ b/test/site.test.js
@@ -8,8 +8,8 @@ test("root loads released flagship controls and local assets", () => {
assert.match(html, /id="theme"/);
assert.match(html, /id="mode"/);
for (const asset of [
- "app-0.3.0.js",
- "siblings-0.3.0.js",
+ "app-0.4.0.js",
+ "siblings-0.4.0.js",
"style-54d303f2.css",
]) {
assert.ok(html.includes(asset));
diff --git a/vendor/doggo-log/commentary.js b/vendor/doggo-log/commentary.js
new file mode 100644
index 0000000..14e8c1b
--- /dev/null
+++ b/vendor/doggo-log/commentary.js
@@ -0,0 +1,77 @@
+// Original commentary; never replaces the caller's message.
+const lines = {
+ debug: [
+ "Sniffing for clues.",
+ "Follow the trace, not the squirrel.",
+ "One breakpoint. Two ears. Maximum attention.",
+ "The missing value has left a scent trail.",
+ "Logging the evidence before chasing the theory.",
+ "The stack trace is our walking route.",
+ "Small reproduction, big detective energy.",
+ "Dallas found a clue. Benji would like to inspect the keyboard.",
+ ],
+ log: [
+ "Filed under things I sniffed.",
+ "Fetch complete. Filing the interesting bit.",
+ "Another breadcrumb for future-us.",
+ "This line has been approved by the tail department.",
+ "A small update with excellent ears.",
+ "Evidence delivered without chewing it.",
+ "Keeping a trail through the code forest.",
+ "Benji brought the log. Dallas brought enthusiasm.",
+ ],
+ info: [
+ "Good to know. Good dog to tell you.",
+ "A useful update, delivered at husky speed.",
+ "The facts have arrived wearing sensible paws.",
+ "Worth knowing before the next zoomie.",
+ "No alarm. Just a well-timed nose boop.",
+ "Status fetched. Tail at a responsible speed.",
+ "A little context saves a lot of barking.",
+ "Dallas and Benji have entered the observability business.",
+ ],
+ success: [
+ "Treat budget approved.",
+ "Good result. Better evidence. Best dog.",
+ "The check passed. Save some applause for monitoring.",
+ "One fewer problem between us and the walk.",
+ "Tail deployment successful.",
+ "The happy path has receipts today.",
+ "Achievement unlocked: boring, repeatable success.",
+ "Dallas celebrates. Benji is already planning the victory lap.",
+ ],
+ warn: [
+ "Suspicious squirrel detected.",
+ "Ears up. This deserves a closer look.",
+ "Something smells odd; inspect before retrying.",
+ "A warning is a breadcrumb, not a dare.",
+ "The tail slowed down for a reason.",
+ "Check the evidence before this becomes an incident.",
+ "Potential trouble has arrived with muddy paws.",
+ "Benji heard something. Dallas recommends checking the logs.",
+ ],
+ error: [
+ "The dog has fetched the incident report.",
+ "Read the first failure before chasing the pack.",
+ "The red light is evidence, not a personality review.",
+ "Pause the zoomies. Find the cause and the rollback.",
+ "This needs a fix, not louder barking.",
+ "Capture the reproduction while the scent is fresh.",
+ "The operation failed. The team still gets kindness.",
+ "Dallas and Benji are standing by with emotional support.",
+ ],
+};
+export function barkLines(level) {
+ if (typeof level !== "string" || !Object.hasOwn(lines, level))
+ throw new RangeError("Choose debug, log, info, success, warn or error.");
+ return [...lines[level]];
+}
+export function commentaryIndex(seed, salt, length) {
+ if (seed === undefined) return 0;
+ let hash = 2166136261;
+ for (const char of salt + ":" + typeof seed + ":" + seed) {
+ hash ^= char.codePointAt(0);
+ hash = Math.imul(hash, 16777619) >>> 0;
+ }
+ return hash % length;
+}
diff --git a/vendor/doggo-log/index.js b/vendor/doggo-log/index.js
index 53238cf..c19772b 100644
--- a/vendor/doggo-log/index.js
+++ b/vendor/doggo-log/index.js
@@ -1,13 +1,7 @@
import { format } from "node:util";
import { levels } from "./levels.js";
-const barks = {
- debug: "Sniffing for clues.",
- log: "Filed under things I sniffed.",
- info: "Good to know. Good dog to tell you.",
- success: "Treat budget approved.",
- warn: "Suspicious squirrel detected.",
- error: "The dog has fetched the incident report.",
-};
+import { barkLines, commentaryIndex } from "./commentary.js";
+export { barkLines };
/**
* Create a small logger. Methods return the emitted line, or undefined if filtered.
@@ -19,6 +13,8 @@ export function createDogLogger(options = {}) {
const config = {
emoji: true,
bark: false,
+ barkMode: "classic",
+ seed: undefined,
color: false,
timestamp: false,
json: false,
@@ -62,6 +58,15 @@ export function createDogLogger(options = {}) {
);
if (typeof config.write !== "function" || typeof config.clock !== "function")
throw new TypeError("write and clock must be functions.");
+ if (!["classic", "rotate"].includes(config.barkMode))
+ throw new RangeError("barkMode must be classic or rotate.");
+ if (
+ config.seed !== undefined &&
+ typeof config.seed !== "string" &&
+ !(typeof config.seed === "number" && Number.isFinite(config.seed))
+ )
+ throw new TypeError("seed must be a string or finite number.");
+ const counters = {};
const logger = Object.fromEntries(
Object.entries(levels).map(([level, style]) => [
level,
@@ -69,7 +74,18 @@ export function createDogLogger(options = {}) {
if (config.quiet || style.rank < levels[config.level].rank)
return undefined;
const message = format(...args);
- const commentary = config.bark ? barks[level] : undefined;
+ const pool = config.bark ? barkLines(level) : undefined;
+ const index =
+ config.barkMode === "rotate"
+ ? (commentaryIndex(
+ config.seed,
+ config.prefix + ":" + level,
+ pool?.length ?? 1,
+ ) +
+ (counters[level] ?? 0)) %
+ (pool?.length ?? 1)
+ : 0;
+ const commentary = pool?.[index];
let timestamp;
if (config.timestamp) {
const date = config.clock();
@@ -103,6 +119,8 @@ export function createDogLogger(options = {}) {
if (config.color) line = `\u001b[${style.color}m${line}\u001b[0m`;
}
config.write(line, level);
+ if (config.bark)
+ counters[level] = ((counters[level] ?? 0) + 1) % pool.length;
return line;
},
]),
diff --git a/vendor/error-translator/definitions.js b/vendor/error-translator/definitions.js
index 7474583..ebad2b3 100644
--- a/vendor/error-translator/definitions.js
+++ b/vendor/error-translator/definitions.js
@@ -1,5 +1,7 @@
+import { extraDefinitions } from "./extra-definitions.js";
// Keep technical guidance here so contributors can add errors without changing logic.
export const definitions = {
+ ...extraDefinitions,
ECONNREFUSED: {
title: "Connection refused",
explanation:
diff --git a/vendor/error-translator/extra-definitions.js b/vendor/error-translator/extra-definitions.js
new file mode 100644
index 0000000..92decce
--- /dev/null
+++ b/vendor/error-translator/extra-definitions.js
@@ -0,0 +1,494 @@
+// Original explanations and practical checks; no automatic remediation.
+export const extraDefinitions = {
+ EACCES: {
+ title: "Permission denied",
+ explanation: "The process lacks permission for the requested operation.",
+ likelyCauses: [
+ "A directory or file belongs to another account",
+ "The application runs under a restricted service identity",
+ ],
+ suggestions: [
+ "Check the service identity and permissions on each parent directory.",
+ "Grant only the access the application needs; avoid running everything as administrator.",
+ ],
+ },
+ EPERM: {
+ title: "Operation not permitted",
+ explanation:
+ "The operating system rejected an operation even though the target may exist.",
+ likelyCauses: [
+ "A protected resource or locked file",
+ "An operation forbidden by the current platform or identity",
+ ],
+ suggestions: [
+ "Inspect the operation, ownership and platform restrictions.",
+ "Close the process holding the resource and retry only if the operation is intended.",
+ ],
+ },
+ EEXIST: {
+ title: "Target already exists",
+ explanation:
+ "A creation operation requires a new target, but that target already exists.",
+ likelyCauses: ["A repeated initialization step", "A filename collision"],
+ suggestions: [
+ "Check the target before creating it and make initialization idempotent.",
+ "Use a distinct name rather than deleting an existing target blindly.",
+ ],
+ },
+ ENOTDIR: {
+ title: "Expected a directory",
+ explanation:
+ "A path component used as a directory is actually another kind of file.",
+ likelyCauses: [
+ "A file appears in the middle of the path",
+ "A configuration value points to the wrong target",
+ ],
+ suggestions: [
+ "Inspect each path component and its type.",
+ "Correct the directory configuration before retrying.",
+ ],
+ },
+ EISDIR: {
+ title: "Expected a file",
+ explanation: "The operation received a directory where it expected a file.",
+ likelyCauses: [
+ "The filename is missing from a configured path",
+ "A directory was passed to a file operation",
+ ],
+ suggestions: [
+ "Check the complete path and inspect the target type.",
+ "Use the appropriate directory API when working with directories.",
+ ],
+ },
+ ENOTEMPTY: {
+ title: "Directory is not empty",
+ explanation:
+ "A directory operation requires an empty target, but entries remain.",
+ likelyCauses: [
+ "Generated files remain in the directory",
+ "Another process is still writing files",
+ ],
+ suggestions: [
+ "List the remaining entries and identify their owner.",
+ "Remove only confirmed disposable content or choose a new directory.",
+ ],
+ },
+ EMFILE: {
+ title: "Too many open files",
+ explanation: "The process has exhausted its available file descriptors.",
+ likelyCauses: [
+ "Handles are not closed",
+ "Too many files or sockets are opened concurrently",
+ ],
+ suggestions: [
+ "Close handles in cleanup paths and bound concurrent work.",
+ "Inspect descriptor usage before considering a higher process limit.",
+ ],
+ },
+ ENFILE: {
+ title: "System file table exhausted",
+ explanation: "The system cannot allocate another file handle.",
+ likelyCauses: [
+ "Many processes hold open files",
+ "The host has reached a system resource limit",
+ ],
+ suggestions: [
+ "Inspect host-wide handle usage and identify runaway processes.",
+ "Reduce concurrency and resolve leaks before changing system limits.",
+ ],
+ },
+ ENOSPC: {
+ title: "No space left",
+ explanation:
+ "The filesystem cannot allocate the space or resources required by the operation.",
+ likelyCauses: [
+ "The volume is full",
+ "Filesystem quotas or resource limits are exhausted",
+ ],
+ suggestions: [
+ "Check free space, quotas and the volume receiving the write.",
+ "Clear verified disposable data and investigate rapid growth.",
+ ],
+ },
+ EROFS: {
+ title: "Read-only filesystem",
+ explanation: "The target filesystem does not permit writes.",
+ likelyCauses: [
+ "A read-only container mount",
+ "A protected or recovery-mounted volume",
+ ],
+ suggestions: [
+ "Inspect mount settings and choose an intended writable data location.",
+ "Change mount configuration only when the write is part of the deployment design.",
+ ],
+ },
+ EBUSY: {
+ title: "Resource busy",
+ explanation:
+ "A resource cannot complete the requested operation while another operation holds it.",
+ likelyCauses: [
+ "A mounted or open resource",
+ "A concurrent operation has not finished",
+ ],
+ suggestions: [
+ "Identify the process or operation holding the resource.",
+ "Wait for its completion or coordinate ownership instead of looping without a limit.",
+ ],
+ },
+ EXDEV: {
+ title: "Cross-device operation",
+ explanation:
+ "An operation such as a rename crosses filesystem boundaries that it cannot cross directly.",
+ likelyCauses: [
+ "Temporary and destination files live on different volumes",
+ "A container mount creates a filesystem boundary",
+ ],
+ suggestions: [
+ "Create temporary files on the destination filesystem when atomic rename is required.",
+ "Use an intentional copy-and-cleanup workflow when atomic movement is unnecessary.",
+ ],
+ },
+ EPIPE: {
+ title: "Broken pipe",
+ explanation:
+ "The application wrote to a stream whose receiving end has closed.",
+ likelyCauses: [
+ "A downstream command exited",
+ "A peer disconnected before the write completed",
+ ],
+ suggestions: [
+ "Handle stream errors and stop producing data when the consumer closes.",
+ "Distinguish an expected pipeline close from an unexpected connection failure.",
+ ],
+ },
+ ECONNRESET: {
+ title: "Connection reset",
+ explanation:
+ "A connection was forcibly closed while the application was using it.",
+ likelyCauses: [
+ "The remote peer closed the connection",
+ "An intermediary interrupted the session",
+ ],
+ suggestions: [
+ "Inspect peer logs, timeout settings and recent connection changes.",
+ "Retry only safe operations with a bounded backoff and avoid duplicate side effects.",
+ ],
+ },
+ ETIMEDOUT: {
+ title: "Operation timed out",
+ explanation:
+ "A connection or operation did not finish within its allowed time.",
+ likelyCauses: [
+ "An unreachable or slow peer",
+ "A timeout shorter than the expected operation",
+ ],
+ suggestions: [
+ "Check network reachability, service latency and the configured timeout.",
+ "Use bounded retries only for operations that are safe to repeat.",
+ ],
+ },
+ ENOTFOUND: {
+ title: "Name lookup failed",
+ explanation:
+ "The requested hostname could not be resolved by the lookup operation.",
+ likelyCauses: [
+ "A misspelled hostname",
+ "Resolver configuration or lookup resources failed",
+ ],
+ suggestions: [
+ "Check the exact hostname and DNS behavior from the same runtime environment.",
+ "Do not assume every lookup failure proves the hostname does not exist.",
+ ],
+ },
+ EAI_AGAIN: {
+ title: "Temporary name lookup failure",
+ explanation: "Name resolution reported a temporary failure.",
+ likelyCauses: [
+ "A resolver is temporarily unavailable",
+ "A transient network or DNS problem",
+ ],
+ suggestions: [
+ "Check the resolver and network from the affected host.",
+ "Apply bounded retry with backoff and report persistent failures.",
+ ],
+ },
+ EADDRNOTAVAIL: {
+ title: "Local address unavailable",
+ explanation:
+ "The application tried to use an address that is not available on the local system.",
+ likelyCauses: [
+ "Binding to an address on another host",
+ "An interface address changed",
+ ],
+ suggestions: [
+ "Inspect local interfaces and the configured bind address.",
+ "Use the intended local interface rather than a remote destination address.",
+ ],
+ },
+ ECONNABORTED: {
+ title: "Connection aborted",
+ explanation: "A connection was aborted before the operation completed.",
+ likelyCauses: [
+ "A connection failure during setup or transfer",
+ "A local or intermediary cancellation",
+ ],
+ suggestions: [
+ "Inspect the failure timing and both ends of the connection.",
+ "Handle cancellation separately from application errors when appropriate.",
+ ],
+ },
+ ENETUNREACH: {
+ title: "Network unreachable",
+ explanation: "The host has no usable route to the requested network.",
+ likelyCauses: [
+ "Missing route or disconnected interface",
+ "Network isolation in a container or host",
+ ],
+ suggestions: [
+ "Inspect connectivity and routing in the application environment.",
+ "Confirm the destination network is intended to be reachable.",
+ ],
+ },
+ EHOSTUNREACH: {
+ title: "Host unreachable",
+ explanation:
+ "The destination host could not be reached through the available network path.",
+ likelyCauses: [
+ "The destination is unavailable",
+ "Routing or filtering blocks the path",
+ ],
+ suggestions: [
+ "Check the destination address and routing from the affected host.",
+ "Inspect network policy rather than disabling protection blindly.",
+ ],
+ },
+ EINVAL: {
+ title: "Invalid argument",
+ explanation: "The operating system rejected an argument to the operation.",
+ likelyCauses: [
+ "A value outside the operation contract",
+ "A platform-specific unsupported value",
+ ],
+ suggestions: [
+ "Inspect the syscall and the exact values passed to it.",
+ "Validate values against the API and platform requirements.",
+ ],
+ },
+ ABORT_ERR: {
+ title: "Operation aborted",
+ explanation: "An operation was cancelled through an abort mechanism.",
+ likelyCauses: [
+ "An AbortSignal was triggered",
+ "A caller timeout cancelled the operation",
+ ],
+ suggestions: [
+ "Inspect who owns the abort controller and when cancellation occurred.",
+ "Treat expected cancellation separately and clean up resources.",
+ ],
+ },
+ ERR_INVALID_ARG_TYPE: {
+ title: "Wrong argument type",
+ explanation: "A Node API received an argument of the wrong type.",
+ likelyCauses: [
+ "A missing or incorrect value",
+ "An unexpected return type passed to another API",
+ ],
+ suggestions: [
+ "Read the parameter name in the error and inspect its actual value.",
+ "Validate the argument at the boundary where it enters your application.",
+ ],
+ },
+ ERR_INVALID_ARG_VALUE: {
+ title: "Invalid argument value",
+ explanation: "An argument has a value the API does not accept.",
+ likelyCauses: [
+ "A valid type with an unsupported value",
+ "An invalid option combination",
+ ],
+ suggestions: [
+ "Inspect the named parameter and documented accepted values.",
+ "Validate options before calling the API.",
+ ],
+ },
+ ERR_OUT_OF_RANGE: {
+ title: "Value outside allowed range",
+ explanation: "A numeric or size argument falls outside the API range.",
+ likelyCauses: [
+ "An invalid offset or length",
+ "A computed limit exceeds the supported bounds",
+ ],
+ suggestions: [
+ "Check the reported minimum and maximum and the calculation producing the value.",
+ "Add boundary tests for zero, limits and oversized input.",
+ ],
+ },
+ ERR_HTTP_HEADERS_SENT: {
+ title: "Headers already sent",
+ explanation:
+ "The server tried to change headers after the response headers had been sent.",
+ likelyCauses: [
+ "Two code paths respond to the same request",
+ "An asynchronous callback runs after the response is complete",
+ ],
+ suggestions: [
+ "Trace every response path and return after sending the response.",
+ "Coordinate asynchronous work so only one path owns the final response.",
+ ],
+ },
+ ERR_HTTP_INVALID_STATUS_CODE: {
+ title: "Invalid HTTP status",
+ explanation: "The response status code is outside the accepted API range.",
+ likelyCauses: [
+ "An undefined value becomes a status",
+ "A business code is mistaken for an HTTP status",
+ ],
+ suggestions: [
+ "Check the status value before assigning or sending it.",
+ "Keep internal result codes separate from HTTP status codes.",
+ ],
+ },
+ ERR_INVALID_URL: {
+ title: "Invalid URL",
+ explanation: "The URL parser cannot interpret the supplied URL.",
+ likelyCauses: [
+ "A malformed absolute URL",
+ "A relative URL without a valid base",
+ ],
+ suggestions: [
+ "Inspect the original input and supply an explicit base for relative paths.",
+ "Validate user-controlled URL inputs before making requests.",
+ ],
+ },
+ ERR_PACKAGE_PATH_NOT_EXPORTED: {
+ title: "Package path not exported",
+ explanation: "A package exports map does not expose the requested subpath.",
+ likelyCauses: [
+ "Importing an internal package file",
+ "A dependency changed its public entry points",
+ ],
+ suggestions: [
+ "Inspect the package exports and use a documented public entry.",
+ "Upgrade or pin compatible dependencies instead of relying on private paths.",
+ ],
+ },
+ ERR_PACKAGE_IMPORT_NOT_DEFINED: {
+ title: "Package import not defined",
+ explanation: "A package imports map has no matching internal specifier.",
+ likelyCauses: [
+ "A missing imports mapping",
+ "A specifier is used outside its intended package scope",
+ ],
+ suggestions: [
+ "Check package.json imports and the exact specifier.",
+ "Keep internal import aliases inside the package that defines them.",
+ ],
+ },
+ ERR_INVALID_PACKAGE_CONFIG: {
+ title: "Invalid package configuration",
+ explanation: "Node could not use the package configuration.",
+ likelyCauses: ["Malformed package.json", "An invalid configuration field"],
+ suggestions: [
+ "Read the named package.json and validate its JSON and relevant fields.",
+ "Check the first configuration error before reinstalling dependencies.",
+ ],
+ },
+ ERR_UNKNOWN_FILE_EXTENSION: {
+ title: "Unknown module file extension",
+ explanation:
+ "The module loader does not recognize the file extension in this context.",
+ likelyCauses: [
+ "An unsupported file type is imported",
+ "The required build or loader is missing",
+ ],
+ suggestions: [
+ "Check the imported extension and the runtime module configuration.",
+ "Compile to a supported format or use a documented loader setup.",
+ ],
+ },
+ ERR_IMPORT_ATTRIBUTE_MISSING: {
+ title: "Required import attribute missing",
+ explanation: "A module import requires an attribute that was not provided.",
+ likelyCauses: [
+ "A JSON import lacks the required type attribute",
+ "An import was copied from a different runtime version",
+ ],
+ suggestions: [
+ "Check the module type and import syntax for your supported Node version.",
+ "Use the required import attributes or an appropriate file-reading API.",
+ ],
+ },
+ ERR_STREAM_WRITE_AFTER_END: {
+ title: "Write after stream end",
+ explanation: "Data was written after the writable stream had ended.",
+ likelyCauses: [
+ "A late callback writes to a closed stream",
+ "Multiple owners end and write the same stream",
+ ],
+ suggestions: [
+ "Trace the stream lifecycle and coordinate the writer with completion.",
+ "Test delayed callbacks and ensure end is called once work is complete.",
+ ],
+ },
+ ERR_STREAM_DESTROYED: {
+ title: "Stream destroyed",
+ explanation: "An operation used a stream after it had been destroyed.",
+ likelyCauses: [
+ "A prior error destroyed the stream",
+ "Cleanup ran while work was still pending",
+ ],
+ suggestions: [
+ "Inspect the original stream error and cancellation timing.",
+ "Stop dependent work when the stream closes or is destroyed.",
+ ],
+ },
+ ERR_SOCKET_BAD_PORT: {
+ title: "Invalid socket port",
+ explanation: "A socket port value is not accepted by the API.",
+ likelyCauses: [
+ "A malformed environment variable",
+ "A number outside the port range",
+ ],
+ suggestions: [
+ "Validate and parse the port configuration before creating the connection.",
+ "Do not substitute a silent default for a visibly invalid production setting.",
+ ],
+ },
+ SyntaxError: {
+ title: "Invalid JavaScript syntax",
+ explanation: "The parser cannot interpret the source or input syntax.",
+ likelyCauses: [
+ "Malformed JavaScript or JSON",
+ "Syntax unsupported by the current runtime",
+ ],
+ suggestions: [
+ "Read the first reported location and inspect nearby delimiters and tokens.",
+ "Confirm the input format and runtime version before changing unrelated code.",
+ ],
+ },
+ ReferenceError: {
+ title: "Name is not available",
+ explanation:
+ "An expression refers to a binding that is not available in its current scope or initialization state.",
+ likelyCauses: [
+ "A misspelled or out-of-scope variable",
+ "Access before initialization",
+ ],
+ suggestions: [
+ "Inspect the referenced name and its scope at the reported line.",
+ "Check initialization order and add a focused regression test.",
+ ],
+ },
+ RangeError: {
+ title: "Value exceeds a permitted range",
+ explanation:
+ "An operation received a value outside its allowed range or exhausted a range-related resource.",
+ likelyCauses: [
+ "An invalid numeric size or index",
+ "Excessive recursion or allocation",
+ ],
+ suggestions: [
+ "Read the exact message to distinguish numeric bounds from stack exhaustion.",
+ "Inspect boundary values and recursion termination rather than treating every RangeError alike.",
+ ],
+ },
+};
diff --git a/vendor/error-translator/index.js b/vendor/error-translator/index.js
index 3e94828..a20ed7e 100644
--- a/vendor/error-translator/index.js
+++ b/vendor/error-translator/index.js
@@ -8,6 +8,55 @@ export function translateErrors(errors, options = {}) {
return Array.from(errors, (error) => translateError(error, options));
}
const duckLines = {
+ EACCES: "The file has a guest list. Your process is not on it.",
+ EPERM: "The operating system has declined your ambitious proposal.",
+ EEXIST: "The target is already here and has not agreed to be replaced.",
+ ENOTDIR: "That hallway is actually a cupboard.",
+ EISDIR: "You asked for one file and received the whole neighborhood.",
+ ENOTEMPTY: "The directory still has roommates.",
+ EMFILE: "Your process opened every door and forgot to close them.",
+ ENFILE: "The host has run out of chairs for file handles.",
+ ENOSPC: "The disk has eaten the final snack.",
+ EROFS: "This filesystem is a museum: look, but do not rearrange.",
+ EBUSY: "The resource is in another meeting.",
+ EXDEV: "The rename has reached the border and needs a different passport.",
+ EPIPE: "The audience left before your stream finished the speech.",
+ ECONNRESET: "The other end hung up without the usual pleasantries.",
+ ETIMEDOUT: "The operation is late and has not brought a believable excuse.",
+ ENOTFOUND: "The hostname is not answering roll call.",
+ EAI_AGAIN: "DNS requests a brief intermission, not eternal optimism.",
+ EADDRNOTAVAIL: "That address does not live on this machine.",
+ ECONNABORTED: "The connection left before the conversation was finished.",
+ ENETUNREACH: "The packet has no road to the destination.",
+ EHOSTUNREACH:
+ "The route exists; the destination is still playing hard to reach.",
+ EINVAL: "The argument has failed the entrance interview.",
+ ABORT_ERR: "The caller pulled the emergency stop. Ask why before restarting.",
+ ERR_INVALID_ARG_TYPE:
+ "The parameter ordered a string and received a surprise guest.",
+ ERR_INVALID_ARG_VALUE: "The type is correct. Its life choices are not.",
+ ERR_OUT_OF_RANGE: "The number wandered beyond the approved fence.",
+ ERR_HTTP_HEADERS_SENT:
+ "The response left the station. You cannot redecorate its headers now.",
+ ERR_HTTP_INVALID_STATUS_CODE:
+ "That number is not a useful mood for an HTTP response.",
+ ERR_INVALID_URL:
+ "The address has syntax ambitions but no navigable destination.",
+ ERR_PACKAGE_PATH_NOT_EXPORTED: "You are trying the package staff entrance.",
+ ERR_PACKAGE_IMPORT_NOT_DEFINED:
+ "The alias has not been introduced to this package.",
+ ERR_INVALID_PACKAGE_CONFIG:
+ "The package instructions have become abstract art.",
+ ERR_UNKNOWN_FILE_EXTENSION: "The loader does not speak this file dialect.",
+ ERR_IMPORT_ATTRIBUTE_MISSING:
+ "The import arrived without its required name badge.",
+ ERR_STREAM_WRITE_AFTER_END:
+ "The stream said goodbye. Your callback is still talking.",
+ ERR_STREAM_DESTROYED: "The stream has retired. Please respect its notice.",
+ ERR_SOCKET_BAD_PORT: "The port number did not pass the harbor inspection.",
+ SyntaxError: "The parser has found punctuation with questionable intentions.",
+ ReferenceError: "That name is not available for comment in this scope.",
+ RangeError: "The value or call stack has climbed over the fence.",
ECONNREFUSED: "Your app knocked. The service has apparently gone for coffee.",
ENOENT: "The file is playing hide-and-seek. It is currently winning.",
EADDRINUSE: "Two servers reserved the same chair. Only one gets to sit.",
@@ -114,3 +163,8 @@ export function renderTranslation(result) {
: []),
].join("\n\n");
}
+
+/** Read the built-in guidance as independent structured records. */
+export function errorCatalog(options = {}) {
+ return listErrors().map((code) => translateError({ code }, options));
+}
diff --git a/vendor/excuse-js/excuses.js b/vendor/excuse-js/excuses.js
index 4281e67..50f58c9 100644
--- a/vendor/excuse-js/excuses.js
+++ b/vendor/excuse-js/excuses.js
@@ -1,58 +1,157 @@
-// Original, workplace-safe phrases. Add new categories and phrases here.
+// Original, workplace-safe phrases. Each category has twelve distinct excuses.
export const phrases = {
deployment: [
"The deployment passed rehearsal and requested a stunt double for production.",
"The release arrived on time. Its dependencies are still finding parking.",
"The deployment is waiting for the server to finish its dramatic entrance.",
+ "The release notes say smooth landing. The landing gear has requested clarification.",
+ "The deploy button has confidence. The health check has follow-up questions.",
+ "The release is technically live and emotionally still in staging.",
+ "Our rollback plan has achieved the rare status of being read before the incident.",
+ "The canary found a problem and would like its consulting fee in seeds.",
+ "The release was ready until production introduced itself.",
+ "We shipped the fix. The monitoring dashboard is deciding whether to clap.",
+ "The deployment arrived wearing a parachute. This is considered progress.",
+ "The server accepted the release but declined to endorse its lifestyle.",
],
build: [
"The compiler found a typo and promoted it to a company-wide announcement.",
"The build is assembling a committee to discuss the missing semicolon.",
"The artifacts are ready; the build is negotiating custody.",
+ "The compiler is proofreading a novel we thought was a configuration file.",
+ "The build cache remembers a different version of reality.",
+ "The bundle got smaller after we removed the feature that made it work.",
+ "The build system asked for a dependency and received a family reunion.",
+ "One warning invited its friends and now the terminal needs chairs.",
+ "The clean build is clean because it contains no artifacts yet.",
+ "The compiler rejected the argument that it worked yesterday.",
+ "Our build pipeline is a conveyor belt with trust issues.",
+ "The source maps know where the problem is. They are being dramatic about it.",
],
production: [
"Production interpreted the happy path as an optional scenic route.",
"The server is performing an unscheduled demonstration of our recovery plan.",
"Production has requested that our assumptions submit identification.",
+ "Production has discovered an edge case shaped exactly like our largest customer.",
+ "The incident is stable. Our coffee consumption is scaling horizontally.",
+ "The dashboards are green because the broken metric stopped reporting.",
+ "Production read the happy-path test and chose creative freedom.",
+ "The service is healthy in the same way a husky is quiet before stealing a sock.",
+ "We have narrowed the cause to the part of the system currently existing.",
+ "The fallback works. It has been promoted to the main plan without a ceremony.",
+ "The alert arrived before the customer email. We are counting that as a win.",
+ "Our recovery estimate is being reviewed by the concept of time.",
],
bug: [
"The edge case moved to the center and brought furniture.",
"That variable has been exploring career opportunities outside its scope.",
"The bug declined to reproduce until the screen-sharing session began.",
+ "The bug is intermittent, which is engineer for witness protection.",
+ "The reproduction is minimal. The emotional damage is not.",
+ "The issue disappeared while screen sharing and returned to clock in afterward.",
+ "The edge case has built a small apartment in the main path.",
+ "We found the bug. It was wearing a feature flag as a disguise.",
+ "The fix is one line. The explanation requires a whiteboard and snacks.",
+ "The bug survived three refactors and now qualifies for seniority.",
+ "The stack trace has provided directions to a place we never wanted to visit.",
+ "The defect is resolved in the branch where optimism is enabled.",
],
api: [
"The API sent a response, but it used the envelope for the payload.",
"The endpoint is observing a moment of silence for our timeout setting.",
"The contract was clear. The response preferred interpretive dance.",
+ "The endpoint returned success with the confidence of an empty response body.",
+ "The API contract and implementation are currently seeing other people.",
+ "The request timed out while the server composed a thoughtful silence.",
+ "The response schema evolved during the meeting about freezing the schema.",
+ "The retry succeeded in creating the same problem twice.",
+ "The status code is correct. The payload is expressing itself artistically.",
+ "The endpoint is idempotent on days when nobody asks it twice.",
+ "The client sent a request. The server returned a character development arc.",
+ "The integration test has discovered that both sides mean something different by optional.",
],
database: [
"The database is taking a full-table stroll to consider our question.",
"The query asked for one row and accidentally invited the whole neighborhood.",
"The migration packed the columns but forgot the forwarding address.",
+ "The query planner chose the scenic route through every row.",
+ "The migration passed locally on a database containing exactly our confidence.",
+ "The index exists. The query would prefer to meet all the data personally.",
+ "The connection pool is full of connections discussing their retirement.",
+ "The transaction is waiting on a lock that is waiting on a conversation.",
+ "The backup completed. We are now attempting the bold sequel: restore.",
+ "The schema is flexible because we forgot where it was documented.",
+ "The slow query has enough history to apply for heritage status.",
+ "The data is consistent about disagreeing with the application.",
],
deadline: [
"The estimate included coding time, but the calendar added plot twists.",
"The deadline arrived before the requirements finished introducing themselves.",
"The last ten percent has applied for its own project manager.",
+ "The estimate included development, testing and a completely fictional Tuesday.",
+ "The smallest deliverable grew after someone used the word just.",
+ "The timeline assumed dependencies would arrive with their shoes on.",
+ "We can deliver quickly if the definition of delivered stops moving.",
+ "The calendar is immutable. The scope is doing parkour.",
+ "The critical path has stopped to attend a planning meeting.",
+ "The deadline is realistic in the timezone where requirements never change.",
+ "We reduced the scope. It is now only moderately larger than the week.",
+ "The schedule has a buffer. Unfortunately it is written in pencil.",
],
testing: [
"The test fixture has developed stronger opinions than the specification.",
"The flaky test only accepts results delivered under a full cache.",
"The assertion expected certainty; the asynchronous task offered suspense.",
+ "The test passed because the assertion was exceptionally supportive.",
+ "The fixture describes a world with fewer users and better manners.",
+ "The flaky test is conducting a long-term study of team patience.",
+ "Coverage went up after we tested the part that says hello.",
+ "The integration test has introduced the units to each other. They are arguing.",
+ "The mock is perfect at simulating the service we wish we had.",
+ "The regression test passed and immediately asked why it was not here earlier.",
+ "The test suite is green. The skipped tests are enjoying a quiet holiday.",
+ "The failing test has a point and an unfortunate way of making it.",
],
network: [
"The packets took a detour to admire the routing table.",
"DNS is still deciding which address feels most like home.",
"The connection is buffering its explanation for the delay.",
+ "DNS is resolving everything except the team's uncertainty.",
+ "The packet took a detour through a router with strong opinions.",
+ "The connection is stable between the moments when it is not.",
+ "The firewall followed the rules. The configuration forgot to read them.",
+ "The timeout was selected by someone who believed all servers live next door.",
+ "The service is reachable from everywhere except the place that needs it.",
+ "The proxy is helping by changing the question before it arrives.",
+ "The request is in transit and has started a travel blog.",
+ "The network is technically up. The route is considering its options.",
],
"merge-conflict": [
"Both branches brought a solution and neither brought a seating chart.",
"Git discovered that two correct answers wanted the same line.",
"The branches agree on the goal and disagree on every comma.",
+ "Both branches improved the same line in incompatible directions.",
+ "The conflict is resolved. The naming debate is still open.",
+ "Git found two versions of the truth and asked us to facilitate.",
+ "The rebase succeeded after we explained time travel to the tests.",
+ "The diff is small if you exclude the file everybody reformatted.",
+ "The merge was automatic until the humans got involved.",
+ "The branch is current with main and several unresolved feelings.",
+ "We chose both changes. The compiler would like us to choose again.",
+ "The conflict marker survived review by pretending to be punctuation.",
],
demo: [
"The demo saw an audience and switched from software to performance art.",
"The prototype is saving its best behavior for the recording we forgot to start.",
"The live demo has added a surprise question to the presentation.",
+ "The demo works best when the audience is a test fixture.",
+ "The projector has detected confidence and activated its troubleshooting mode.",
+ "The live demo is a distributed system consisting of nerves and Wi-Fi.",
+ "The happy path worked until someone clicked the interesting button.",
+ "The offline fallback is now the star of the presentation.",
+ "The demo data is realistic enough to have missing fields.",
+ "The browser refreshed and introduced us to the unsaved version.",
+ "The feature worked backstage. It is negotiating public appearances.",
+ "The rehearsal was flawless. The room has different opinions about networking.",
],
};
diff --git a/vendor/excuse-js/index.js b/vendor/excuse-js/index.js
index 8a74ff0..4b211f6 100644
--- a/vendor/excuse-js/index.js
+++ b/vendor/excuse-js/index.js
@@ -1,5 +1,7 @@
import { phrases } from "./excuses.js";
const nextSteps = {
+ "merge-conflict":
+ "Understand both changes, resolve the conflict deliberately, then run tests for the combined behavior. Git does not accept a coin toss.",
deployment:
"Check the release diff and rollback plan before delivering this line.",
testing:
@@ -90,3 +92,13 @@ export function excuseBatch(category = "deployment", options = {}) {
(_, i) => pool[(start + i) % pool.length],
);
}
+
+/** Read a category without consuming randomness; the returned catalog is independent. */
+export function listExcuses(category = "deployment") {
+ if (typeof category !== "string" || !category.trim())
+ throw new TypeError("Category must be a nonempty string.");
+ const key = category.trim().toLowerCase();
+ if (!Object.hasOwn(phrases, key))
+ throw new RangeError("Unknown category: " + category);
+ return [...phrases[key]];
+}
diff --git a/vendor/ship-it-meter/index.js b/vendor/ship-it-meter/index.js
index 2a33b16..72cbd77 100644
--- a/vendor/ship-it-meter/index.js
+++ b/vendor/ship-it-meter/index.js
@@ -187,3 +187,113 @@ export function preflight(input = {}) {
);
return { ...result, actions };
}
+
+export { releaseScenarios, scenarioEvidence } from "./scenarios.js";
+/** Explain the gate as ordered work with observable completion criteria. */
+export function releasePlan(input = {}, options = {}) {
+ const gate = releaseGate(input, options);
+ const tasks = [];
+ const add = (id, priority, title, verify) =>
+ tasks.push({ id, priority, title, verify });
+ if (input.build !== true)
+ add(
+ "build",
+ "blocker",
+ "Get a passing build",
+ "Run the release build for the exact commit and record its successful result.",
+ );
+ if (!input.tests)
+ add(
+ "tests",
+ "blocker",
+ "Run a nonempty test suite",
+ "Record the test count and outcome for the release commit.",
+ );
+ if (input.failingTests)
+ add(
+ "failing-tests",
+ "blocker",
+ "Repair failing tests",
+ "Reproduce each failure, fix its cause and rerun the suite.",
+ );
+ if (input.coverage === undefined)
+ add(
+ "coverage-evidence",
+ "blocker",
+ "Measure coverage",
+ "Generate coverage for the release commit and record the percentage.",
+ );
+ else if (input.coverage < 80)
+ add(
+ "coverage-review",
+ "review",
+ "Review untested paths",
+ "Inspect uncovered critical paths and add meaningful tests where needed.",
+ );
+ if (input.criticalIssues)
+ add(
+ "critical-issues",
+ "blocker",
+ "Resolve critical issues",
+ "Record the resolution and verify the affected behavior.",
+ );
+ if (input.lintFailures)
+ add(
+ "lint",
+ "blocker",
+ "Fix lint failures",
+ "Run the configured lint checks successfully.",
+ );
+ if (input.uncommittedChanges)
+ add(
+ "worktree",
+ "blocker",
+ "Reconcile local changes",
+ "Commit or stash intentionally and verify the exact release source.",
+ );
+ if (input.branch && !["main", "master"].includes(input.branch.trim()))
+ add(
+ "branch",
+ "review",
+ "Confirm release branch",
+ "Verify that this branch is the intended release source and that its CI passed.",
+ );
+ if (input.day?.trim().toLowerCase() === "friday")
+ add(
+ "friday",
+ "review",
+ "Confirm weekend support",
+ "Identify monitoring ownership and a tested rollback path.",
+ );
+ if (gate.score < gate.minScore)
+ add(
+ "score",
+ "blocker",
+ "Improve evidence to the configured threshold",
+ "Address score deductions, rerun the checks and reevaluate the gate.",
+ );
+ add(
+ "rollback",
+ "release",
+ "Prepare the return route",
+ "Verify the rollback artifact and procedure before deployment.",
+ );
+ add(
+ "monitoring",
+ "release",
+ "Watch the actual release",
+ "Check health and error metrics after deployment and name an owner.",
+ );
+ const summaries = {
+ "Absolutely Not": "The evidence says stop. The confidence hat can wait.",
+ Questionable:
+ "The release has questions. Answer them before adding confetti.",
+ "Probably Fine": "Close is a useful direction, not a deployment receipt.",
+ "Ship It": "Good evidence. Keep the parachute and watch the landing.",
+ "Suspiciously Ready":
+ "Boring, verified readiness. Give the checks their share of the applause.",
+ };
+ const rank = { blocker: 0, review: 1, release: 2 };
+ tasks.sort((a, b) => rank[a.priority] - rank[b.priority]);
+ return { ...gate, summary: summaries[gate.verdict], tasks };
+}
diff --git a/vendor/ship-it-meter/scenarios.js b/vendor/ship-it-meter/scenarios.js
new file mode 100644
index 0000000..069b8e6
--- /dev/null
+++ b/vendor/ship-it-meter/scenarios.js
@@ -0,0 +1,133 @@
+// Demonstration inputs, never a claim about a real release.
+const scenarios = {
+ ready: {
+ description:
+ "Passing evidence, clean worktree and main branch. Boring is a compliment.",
+ evidence: {
+ tests: 125,
+ coverage: 96,
+ build: true,
+ branch: "main",
+ day: "tuesday",
+ },
+ },
+ friday: {
+ description:
+ "Good evidence on Friday. Confirm rollback and on-call before the victory lap.",
+ evidence: {
+ tests: 125,
+ coverage: 92,
+ build: true,
+ branch: "main",
+ day: "friday",
+ },
+ },
+ "no-evidence": {
+ description:
+ "Confidence is present. Test, coverage and build evidence have not arrived.",
+ evidence: {},
+ },
+ "failed-build": {
+ description:
+ "The build is red. The deploy button is not a color correction tool.",
+ evidence: {
+ tests: 125,
+ coverage: 92,
+ build: false,
+ },
+ },
+ "failing-tests": {
+ description:
+ "Some tests fail. Green deployment lights will not repair assertions.",
+ evidence: {
+ tests: 125,
+ failingTests: 3,
+ coverage: 92,
+ build: true,
+ },
+ },
+ "low-coverage": {
+ description:
+ "A passing build with many untested paths. The bugs appreciate the privacy.",
+ evidence: {
+ tests: 125,
+ coverage: 45,
+ build: true,
+ },
+ },
+ "critical-issue": {
+ description:
+ "A critical issue blocks the gate even when everything else looks cheerful.",
+ evidence: {
+ tests: 125,
+ coverage: 96,
+ build: true,
+ criticalIssues: 1,
+ },
+ },
+ "lint-failures": {
+ description:
+ "Lint failures are still failures, however decorative the semicolons feel.",
+ evidence: {
+ tests: 125,
+ coverage: 96,
+ build: true,
+ lintFailures: 2,
+ },
+ },
+ "dirty-worktree": {
+ description:
+ "The laptop contains uncommitted changes. It is not the release archive.",
+ evidence: {
+ tests: 125,
+ coverage: 96,
+ build: true,
+ uncommittedChanges: true,
+ },
+ },
+ "feature-branch": {
+ description:
+ "Healthy evidence on a feature branch. Verify the intended release source.",
+ evidence: {
+ tests: 125,
+ coverage: 96,
+ build: true,
+ branch: "feature/zoomies",
+ },
+ },
+ "zero-tests": {
+ description:
+ "No tests ran. Zero failures is an especially small achievement here.",
+ evidence: {
+ tests: 0,
+ coverage: 0,
+ build: true,
+ },
+ },
+ "weekend-incident": {
+ description:
+ "A failed build, failing tests and a critical issue. Bring evidence, not a bigger confidence hat.",
+ evidence: {
+ tests: 125,
+ failingTests: 12,
+ coverage: 60,
+ build: false,
+ criticalIssues: 1,
+ day: "saturday",
+ },
+ },
+};
+export function releaseScenarios() {
+ return Object.entries(scenarios).map(([name, value]) => ({
+ name,
+ description: value.description,
+ evidence: { ...value.evidence },
+ }));
+}
+export function scenarioEvidence(name) {
+ if (typeof name !== "string" || !Object.hasOwn(scenarios, name))
+ throw new RangeError(
+ "Unknown scenario. Choose: " + Object.keys(scenarios).join(", "),
+ );
+ return { ...scenarios[name].evidence };
+}