@deployanyway/error-translator 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ - Explain This Failure: real errors and cause chains.
6
+ - Typed API, CLI integration, runnable codebase example and meaningful workflow tests.
7
+ - Stable contracts and migration guidance; original humor stays around accurate facts.
8
+
3
9
  ## 0.4.0
4
10
 
5
11
  46 built-in definitions cover filesystem, permissions, networking, module loading, streams, HTTP response lifecycle and JavaScript errors. Each has a specific explanation, likely causes and practical checks. Rubber-duck commentary has a distinct original line for every supported definition. No commands run automatically and unknown errors remain explicitly unrecognized.
package/MIGRATION.md CHANGED
@@ -9,3 +9,9 @@ Install 0.3.0 with npm. Seeds and exact humorous wording are version-specific. D
9
9
  46 distinct error definitions with explanations, causes and practical checks; original duck commentary for each; structured errorCatalog API and --catalog CLI.
10
10
 
11
11
  Default behavior is retained except that the expanded error catalog now recognizes additional errors. New rotation and release-plan features are opt-in.
12
+
13
+ ## 0.4.0 to stable 1.0.0
14
+
15
+ diagnoseError/renderDiagnosis and diagnostic CLI flags are additive. Explicit code precedence and existing catalog/batch behavior remain. Use diagnosis.original to hand the same error to your existing handler; serializing a diagnosis omits that reference.
16
+
17
+ See README for exact contracts, bounds and failure behavior.
package/README.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # error-translator
2
2
 
3
+ ## Explain This Failure: real errors and cause chains (1.0.0)
4
+
5
+ diagnoseError(error, options) walks actual Error.cause chains with a default eight-layer bound (maxDepth 1–32). It detects cycles, exposes unknown causes honestly and returns structured chain entries with a translation for each layer. It never throws away the original error: result.original is the exact input reference and is deliberately non-enumerable so JSON output excludes it. The caller controls rethrowing, logging and exit behavior.
6
+
7
+ ```js
8
+ import { diagnoseError, renderDiagnosis } from "@deployanyway/error-translator";
9
+ try {
10
+ await loadConfig();
11
+ } catch (error) {
12
+ const diagnosis = diagnoseError(error, { mode: "rubber-duck" });
13
+ console.error(renderDiagnosis(diagnosis));
14
+ throw error; // preserve failure behavior and the original stack
15
+ }
16
+ ```
17
+
18
+ Options: mode plain/rubber-duck, maxDepth, includeStack (default false). Entries contain depth, name, message, optional code, translation and opt-in stack. stopped is complete/cycle/depth-limit; truncated reports incomplete traversal. Arbitrary primitive causes receive an explicit unknown translation. The caller's errors and causes are not mutated. AggregateError.errors is not flattened; this API follows the cause chain.
19
+
20
+ ```sh
21
+ printf '%s' '{"message":"Config failed","cause":{"code":"ENOENT","message":"open config.json"}}' | error-translator --diagnose --json
22
+ node node_modules/@deployanyway/error-translator/examples/explain-failure.mjs ./missing-config.json
23
+ ```
24
+
25
+ --diagnose accepts a JSON error record from stdin or a quoted argument, with --max-depth and opt-in --include-stack. It rejects batch/catalog/list combinations. Translation/diagnosis exits 0; invalid input exits 2. The runnable filesystem example preserves application failure with exit 1. Messages and opt-in stacks can contain private data; this library does not redact them or run fixes.
26
+
27
+ ## Stable v1 contract
28
+
29
+ Node 22.13+ or Node 24. MIT licensed. CLI flags, structured fields, ESM/CommonJS exports and declarations are covered by tests and installed-package checks. Existing 0.4 APIs remain available except the explicitly documented doggo-log redaction/text-context changes. Future incompatible public API changes require a major release; callers should consume structured fields rather than parse jokes. Exact humorous wording and seeded catalog choices are version-specific. No telemetry, external API keys or network service is needed for core use.
30
+
31
+ Run npm test, npm run lint, npm run format:check, npm run coverage, npm run test:types and npm run verify:package from a source checkout. Runnable examples are shipped under examples/. The root demo is https://deployanyway.github.io/.
32
+
3
33
  ## Browse the debugging library (0.4.0)
4
34
 
5
35
  46 built-in definitions cover filesystem, permissions, networking, module loading, streams, HTTP response lifecycle and JavaScript errors. Each has a specific explanation, likely causes and practical checks. Rubber-duck commentary has a distinct original line for every supported definition. No commands run automatically and unknown errors remain explicitly unrecognized.
package/dist/index.cjs CHANGED
@@ -20,8 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.js
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ diagnoseError: () => diagnoseError,
23
24
  errorCatalog: () => errorCatalog,
24
25
  listErrors: () => listErrors,
26
+ renderDiagnosis: () => renderDiagnosis,
25
27
  renderTranslation: () => renderTranslation,
26
28
  translateError: () => translateError,
27
29
  translateErrors: () => translateErrors
@@ -586,6 +588,67 @@ var definitions = {
586
588
  }
587
589
  };
588
590
 
591
+ // src/diagnostics.js
592
+ function diagnoseError(error, options = {}) {
593
+ if (!options || typeof options !== "object" || Array.isArray(options))
594
+ throw new TypeError("Options must be an object.");
595
+ const maxDepth = options.maxDepth ?? 8, includeStack = options.includeStack ?? false;
596
+ if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 32)
597
+ throw new RangeError("maxDepth must be an integer from 1 to 32.");
598
+ if (typeof includeStack !== "boolean")
599
+ throw new TypeError("includeStack must be a boolean.");
600
+ translateError(error, options);
601
+ const seen = /* @__PURE__ */ new Set(), chain = [];
602
+ let current = error, stopped = "complete";
603
+ while (current !== void 0) {
604
+ if (chain.length === maxDepth) {
605
+ stopped = "depth-limit";
606
+ break;
607
+ }
608
+ if (current !== null && typeof current === "object") {
609
+ if (seen.has(current)) {
610
+ stopped = "cycle";
611
+ break;
612
+ }
613
+ seen.add(current);
614
+ }
615
+ const object = current !== null && typeof current === "object" && !Array.isArray(current) ? current : void 0;
616
+ const raw = typeof current === "string" ? current : object?.message;
617
+ const message = typeof raw === "string" ? raw : String(current);
618
+ const name = typeof object?.name === "string" ? object.name : "Error";
619
+ const code = typeof object?.code === "string" ? object.code : void 0;
620
+ const translation = translateError(
621
+ { name, message, ...code ? { code } : {} },
622
+ options
623
+ );
624
+ chain.push({
625
+ depth: chain.length,
626
+ name,
627
+ message,
628
+ ...code ? { code } : {},
629
+ translation,
630
+ ...includeStack && typeof object?.stack === "string" ? { stack: object.stack } : {}
631
+ });
632
+ current = object?.cause;
633
+ }
634
+ const result = { chain, stopped, truncated: stopped !== "complete" };
635
+ Object.defineProperty(result, "original", {
636
+ value: error,
637
+ enumerable: false
638
+ });
639
+ return result;
640
+ }
641
+ function renderDiagnosis(result) {
642
+ if (!result || !Array.isArray(result.chain) || !result.chain.length || !["complete", "cycle", "depth-limit"].includes(result.stopped))
643
+ throw new TypeError("Provide a diagnosis result.");
644
+ return result.chain.map(
645
+ (item, index) => `${index === 0 ? "Failure" : "Caused by"}: ${item.name}${item.code ? " [" + item.code + "]" : ""}: ${item.message}
646
+ ${renderTranslation(item.translation)}${item.stack ? "\n\nOriginal stack:\n" + item.stack : ""}`
647
+ ).join("\n\n---\n\n") + (result.truncated ? `
648
+
649
+ Cause traversal stopped: ${result.stopped}.` : "");
650
+ }
651
+
589
652
  // src/index.js
590
653
  var listErrors = () => Object.keys(definitions);
591
654
  function translateErrors(errors, options = {}) {
@@ -717,8 +780,10 @@ function errorCatalog(options = {}) {
717
780
  }
718
781
  // Annotate the CommonJS export names for ESM import in node:
719
782
  0 && (module.exports = {
783
+ diagnoseError,
720
784
  errorCatalog,
721
785
  listErrors,
786
+ renderDiagnosis,
722
787
  renderTranslation,
723
788
  translateError,
724
789
  translateErrors
@@ -0,0 +1,9 @@
1
+ import {
2
+ translateError,
3
+ renderTranslation,
4
+ } from "@deployanyway/error-translator";
5
+
6
+ const error = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:3000"), {
7
+ code: "ECONNREFUSED",
8
+ });
9
+ console.log(renderTranslation(translateError(error)));
@@ -0,0 +1,11 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { diagnoseError, renderDiagnosis } from "@deployanyway/error-translator";
3
+ const file = process.argv[2] ?? "missing-config.json";
4
+ try {
5
+ readFileSync(file, "utf8");
6
+ console.log("Config read successfully.");
7
+ } catch (cause) {
8
+ const failure = new Error("Application config could not be read", { cause });
9
+ console.error(renderDiagnosis(diagnoseError(failure)));
10
+ process.exitCode = 1;
11
+ }
package/index.d.cts CHANGED
@@ -1,5 +1,11 @@
1
- export type ErrorInput =
2
- string | Error | { code?: string; name?: string; message?: string };
1
+ export interface ErrorRecord {
2
+ code?: string;
3
+ name?: string;
4
+ message?: string;
5
+ stack?: string;
6
+ cause?: unknown;
7
+ }
8
+ export type ErrorInput = string | Error | ErrorRecord;
3
9
  export interface TranslationOptions {
4
10
  mode?: "plain" | "rubber-duck";
5
11
  }
@@ -23,3 +29,26 @@ export function renderTranslation(result: Translation): string;
23
29
  export function listErrors(): string[];
24
30
 
25
31
  export function errorCatalog(options?: TranslationOptions): Translation[];
32
+ export interface DiagnosisOptions extends TranslationOptions {
33
+ maxDepth?: number;
34
+ includeStack?: boolean;
35
+ }
36
+ export interface DiagnosticLayer {
37
+ depth: number;
38
+ name: string;
39
+ message: string;
40
+ code?: string;
41
+ translation: Translation;
42
+ stack?: string;
43
+ }
44
+ export interface Diagnosis {
45
+ readonly original: ErrorInput;
46
+ chain: DiagnosticLayer[];
47
+ stopped: "complete" | "cycle" | "depth-limit";
48
+ truncated: boolean;
49
+ }
50
+ export function diagnoseError(
51
+ error: ErrorInput,
52
+ options?: DiagnosisOptions,
53
+ ): Diagnosis;
54
+ export function renderDiagnosis(result: Diagnosis): string;
package/index.d.ts CHANGED
@@ -1,5 +1,11 @@
1
- export type ErrorInput =
2
- string | Error | { code?: string; name?: string; message?: string };
1
+ export interface ErrorRecord {
2
+ code?: string;
3
+ name?: string;
4
+ message?: string;
5
+ stack?: string;
6
+ cause?: unknown;
7
+ }
8
+ export type ErrorInput = string | Error | ErrorRecord;
3
9
  export interface TranslationOptions {
4
10
  mode?: "plain" | "rubber-duck";
5
11
  }
@@ -23,3 +29,26 @@ export function renderTranslation(result: Translation): string;
23
29
  export function listErrors(): string[];
24
30
 
25
31
  export function errorCatalog(options?: TranslationOptions): Translation[];
32
+ export interface DiagnosisOptions extends TranslationOptions {
33
+ maxDepth?: number;
34
+ includeStack?: boolean;
35
+ }
36
+ export interface DiagnosticLayer {
37
+ depth: number;
38
+ name: string;
39
+ message: string;
40
+ code?: string;
41
+ translation: Translation;
42
+ stack?: string;
43
+ }
44
+ export interface Diagnosis {
45
+ readonly original: ErrorInput;
46
+ chain: DiagnosticLayer[];
47
+ stopped: "complete" | "cycle" | "depth-limit";
48
+ truncated: boolean;
49
+ }
50
+ export function diagnoseError(
51
+ error: ErrorInput,
52
+ options?: DiagnosisOptions,
53
+ ): Diagnosis;
54
+ export function renderDiagnosis(result: Diagnosis): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deployanyway/error-translator",
3
- "version": "0.4.0",
4
- "description": "Plain-English Node.js error explanations and debugging tips, because the stack trace has chosen violence.",
3
+ "version": "1.0.0",
4
+ "description": "Explain real Node.js errors and cause chains with practical debugging checks. The stack trace has chosen violence; the duck brought context.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -27,7 +27,8 @@
27
27
  "dist",
28
28
  "index.d.ts",
29
29
  "index.d.cts",
30
- "MIGRATION.md"
30
+ "MIGRATION.md",
31
+ "examples"
31
32
  ],
32
33
  "engines": {
33
34
  "node": ">=22.13"
@@ -74,7 +75,9 @@
74
75
  "plain-english",
75
76
  "typescript",
76
77
  "humor",
77
- "deployanyway"
78
+ "deployanyway",
79
+ "error-cause",
80
+ "error-handling"
78
81
  ],
79
82
  "main": "./dist/index.cjs",
80
83
  "types": "./index.d.ts",
package/src/cli.js CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  translateErrors,
8
8
  listErrors,
9
9
  errorCatalog,
10
+ diagnoseError,
11
+ renderDiagnosis,
10
12
  } from "./index.js";
11
13
 
12
14
  import { readStdin } from "./input.js";
@@ -21,12 +23,15 @@ try {
21
23
  batch: { type: "boolean" },
22
24
  list: { type: "boolean" },
23
25
  catalog: { type: "boolean" },
26
+ diagnose: { type: "boolean" },
27
+ "include-stack": { type: "boolean" },
28
+ "max-depth": { type: "string" },
24
29
  mode: { type: "string", default: "plain" },
25
30
  },
26
31
  });
27
32
  if (values.help) {
28
33
  console.log(
29
- 'Usage: error-translator <error code or quoted message> [--json] [--mode plain|rubber-duck]\n\nTranslate errors into human-readable guidance. Without arguments, read stdin (256 KiB). --batch reads a JSON array of 1–100 errors. --list lists supported codes. --catalog prints all explanations and checks.\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Print a structured JSON result\n --mode plain|rubber-duck Keep useful guidance; add duck commentary\n\nExamples:\n error-translator ECONNREFUSED\n error-translator "TypeError: value is not a function" --json\n\nExit codes: 0 translation/help/version; 2 invalid arguments.',
34
+ 'Usage: error-translator <error code or quoted message> [--json] [--mode plain|rubber-duck]\n\nTranslate errors into human-readable guidance. --diagnose reads a JSON error/cause record; --max-depth 1..32 bounds it; --include-stack opts into original stacks. Without arguments, read stdin (256 KiB). --batch reads a JSON array of 1–100 errors. --list lists supported codes. --catalog prints all explanations and checks.\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Print a structured JSON result\n --mode plain|rubber-duck Keep useful guidance; add duck commentary\n\nExamples:\n error-translator ECONNREFUSED\n error-translator "TypeError: value is not a function" --json\n\nExit codes: 0 translation/help/version; 2 invalid arguments.',
30
35
  );
31
36
  } else if (values.version) {
32
37
  console.log(
@@ -34,6 +39,24 @@ try {
34
39
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
35
40
  ).version,
36
41
  );
42
+ } else if (values.diagnose) {
43
+ if (values.batch || values.catalog || values.list)
44
+ throw new TypeError(
45
+ "--diagnose cannot combine with --batch, --catalog or --list.",
46
+ );
47
+ const text = positionals.length ? positionals.join(" ") : await readStdin();
48
+ const result = diagnoseError(JSON.parse(text), {
49
+ mode: values.mode,
50
+ includeStack: values["include-stack"] ?? false,
51
+ ...(values["max-depth"] !== undefined
52
+ ? { maxDepth: Number(values["max-depth"]) }
53
+ : {}),
54
+ });
55
+ console.log(
56
+ values.json ? JSON.stringify(result, null, 2) : renderDiagnosis(result),
57
+ );
58
+ } else if (values["include-stack"] || values["max-depth"] !== undefined) {
59
+ throw new TypeError("--include-stack and --max-depth require --diagnose.");
37
60
  } else if (values.catalog) {
38
61
  if (positionals.length || values.batch || values.list)
39
62
  throw new TypeError(
@@ -0,0 +1,79 @@
1
+ import { translateError, renderTranslation } from "./index.js";
2
+
3
+ /** Snapshot a bounded cause chain; preserve the original without serializing it. */
4
+ export function diagnoseError(error, options = {}) {
5
+ if (!options || typeof options !== "object" || Array.isArray(options))
6
+ throw new TypeError("Options must be an object.");
7
+ const maxDepth = options.maxDepth ?? 8,
8
+ includeStack = options.includeStack ?? false;
9
+ if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 32)
10
+ throw new RangeError("maxDepth must be an integer from 1 to 32.");
11
+ if (typeof includeStack !== "boolean")
12
+ throw new TypeError("includeStack must be a boolean.");
13
+ // Validate the root using the established translation contract.
14
+ translateError(error, options);
15
+ const seen = new Set(),
16
+ chain = [];
17
+ let current = error,
18
+ stopped = "complete";
19
+ while (current !== undefined) {
20
+ if (chain.length === maxDepth) {
21
+ stopped = "depth-limit";
22
+ break;
23
+ }
24
+ if (current !== null && typeof current === "object") {
25
+ if (seen.has(current)) {
26
+ stopped = "cycle";
27
+ break;
28
+ }
29
+ seen.add(current);
30
+ }
31
+ const object =
32
+ current !== null && typeof current === "object" && !Array.isArray(current)
33
+ ? current
34
+ : undefined;
35
+ const raw = typeof current === "string" ? current : object?.message;
36
+ const message = typeof raw === "string" ? raw : String(current);
37
+ const name = typeof object?.name === "string" ? object.name : "Error";
38
+ const code = typeof object?.code === "string" ? object.code : undefined;
39
+ const translation = translateError(
40
+ { name, message, ...(code ? { code } : {}) },
41
+ options,
42
+ );
43
+ chain.push({
44
+ depth: chain.length,
45
+ name,
46
+ message,
47
+ ...(code ? { code } : {}),
48
+ translation,
49
+ ...(includeStack && typeof object?.stack === "string"
50
+ ? { stack: object.stack }
51
+ : {}),
52
+ });
53
+ current = object?.cause;
54
+ }
55
+ const result = { chain, stopped, truncated: stopped !== "complete" };
56
+ Object.defineProperty(result, "original", {
57
+ value: error,
58
+ enumerable: false,
59
+ });
60
+ return result;
61
+ }
62
+ export function renderDiagnosis(result) {
63
+ if (
64
+ !result ||
65
+ !Array.isArray(result.chain) ||
66
+ !result.chain.length ||
67
+ !["complete", "cycle", "depth-limit"].includes(result.stopped)
68
+ )
69
+ throw new TypeError("Provide a diagnosis result.");
70
+ return (
71
+ result.chain
72
+ .map(
73
+ (item, index) =>
74
+ `${index === 0 ? "Failure" : "Caused by"}: ${item.name}${item.code ? " [" + item.code + "]" : ""}: ${item.message}\n${renderTranslation(item.translation)}${item.stack ? "\n\nOriginal stack:\n" + item.stack : ""}`,
75
+ )
76
+ .join("\n\n---\n\n") +
77
+ (result.truncated ? `\n\nCause traversal stopped: ${result.stopped}.` : "")
78
+ );
79
+ }
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { definitions } from "./definitions.js";
2
+ export { diagnoseError, renderDiagnosis } from "./diagnostics.js";
2
3
  export const listErrors = () => Object.keys(definitions);
3
4
 
4
5
  /** Translate an ordered, bounded batch without mutating input. */