feat(errors): ERROR_CATALOG + reportError + new codes — v1 prep 2/3#2
Merged
Conversation
Replace raw Error throws at every IO/parse boundary so callers can
match on a stable err.code instead of err.message. Five new codes:
- CONFIG_NOT_FOUND CLI could not auto-discover a settings config
- CONFIG_LOAD_FAILED config file present but import failed (syntax
error, missing dep, …); original on err.cause
- CONFIG_INVALID_EXPORT module loaded but no defineSettings() export
- FILE_READ_FAILED dotenv / K8s manifest file failed to read
(ENOENT, EACCES, …); original on err.cause
- K8S_YAML_PARSE_FAILED diff input did not parse as YAML
Touched modules:
- cli/load-user-config.ts 3 raw Error throws → NodeSettingsError
- loaders/dotenv-file.ts new readDotenvSafe() wraps fs failures
- loaders/dotenv-cascade.ts uses the safe reader
- diff-k8s.ts parseAllDocuments + doc.errors wrapped
- cli/diff.ts parseK8sYaml now inside try; readSource
uses FILE_READ_FAILED
docs/ERRORS.md gains a CLI/loader section + a client-env section
(was missing from the old doc).
src/loaders/vite-env.ts shipped a placeholder loadViteEnv() returning
{} for as long as the package has existed. Real Vite consumers either
read import.meta.env directly or use the @env-kit/node-settings/vite
plugin — nobody benefits from the stub, and exporting it suggests a
working integration that isn't there.
Removed:
- src/loaders/vite-env.ts (placeholder)
- src/utils/index.ts (unused barrel, no consumers)
Public API change (breaking): loadViteEnv is gone. Slated for v1.0.0.
Also tightened AGENTS.md's import map and file map (the loaders entry
was missing loadDotenvCascade and dotenv-cascade.ts entirely; utils
was missing zod-issues.ts). Refreshed api-surface snapshots.
Companion fix-ups for the two preceding commits: - scripts/verify-dist.mjs no longer expects loadViteEnv in REQUIRED_ROOT (was removed in the previous commit). - src/cli/diff.ts uses 'YAML input' to match the casing already used in src/diff-k8s.ts. The change rode along with the rest of the punctuation pass on the original branch, but it referenced raise(FILE_READ_FAILED, ...) calls that only exist on this branch — so it lands here instead. (Both are the trailing halves of the original c9882b4 / 866adb5 commits; the parts that didn't depend on this PR's diff went into PR #1.)
Refactors error handling around a single source of truth — the new
ERROR_CATALOG — so codes, severity, titles, doc anchors, and the
`docs/ERRORS.md` table never drift apart.
API additions (public):
- ERROR_CATALOG: Record<code, { severity, title, docsAnchor }>.
NodeSettingsErrorCode is now derived from its keys, so adding an
entry extends the union with no other edits.
- ErrorSeverity = 'config' | 'runtime' | 'io' | 'usage'. Lets consumers
route to the right alarm channel without hard-coding code lists.
- NodeSettingsError gains .severity, .title, .docsUrl getters that
read from the catalog at runtime.
- reportError(err, { docsBase? }): ErrorReport — distils any throw
(NodeSettingsError, ZodError, plain Error) into a JSON-serialisable
record with code, severity, title, message, hint?, docsUrl,
issues[]?, cause?.
- DEFAULT_DOCS_BASE — overridable docs URL base.
Internal cleanup:
- cli/validate.ts's local serializeError() removed; the subcommand
calls reportError(err) directly. The --format=json shape gains
severity, title, docsUrl, cause (additive, not breaking JSON
consumers).
- The previously-untyped 'ENV_FILE_NOT_FOUND' literal in validate.ts
is now FILE_READ_FAILED, matching the existing catalog code.
Tooling:
- scripts/verify-errors.mjs — fails if any catalog entry has no
raise() call site, any anchor is missing from docs/ERRORS.md, or
the auto-generated catalog section drifts. Wired into pnpm verify
as a 9th layer.
- scripts/generate-errors-doc.mjs (pnpm gen:errors-doc) — regenerates
the auto-managed catalog table in docs/ERRORS.md between BEGIN/END
markers.
- scripts/lib/errors-doc.mjs — shared rendering used by both scripts
so generator and verifier can't disagree.
docs/ERRORS.md restructured: hand-written intro + severity-grouped
auto-managed catalog. Was four hand-edited tables that could drift
from src/errors.ts; now the source of truth is the catalog and the
doc is regenerated from it.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second of three stacked PRs. Builds the error catalog architecture that distinguishes v1 from v0 — a single source of truth for codes, severity, titles, and docs anchors that everything else derives from.
Breaking changes (slated for v1.0.0):
loadViteEnvremoved. Was a placeholder returning{}; Vite consumers should useimport.meta.envor@env-kit/node-settings/vite.New public API (additive):
ERROR_CATALOG—Record<NodeSettingsErrorCode, { severity, title, docsAnchor }>. The type union is nowkeyof typeof ERROR_CATALOG.ErrorSeverity = "config" | "runtime" | "io" | "usage".NodeSettingsError.severity / .title / .docsUrlgetters read from the catalog.reportError(err, { docsBase? }): ErrorReport— distils any throw (NodeSettingsError,ZodError, plainError) into a JSON-serialisable record withcode,severity,title,message,hint?,docsUrl,issues[]?,cause?.DEFAULT_DOCS_BASE.CONFIG_NOT_FOUND,CONFIG_LOAD_FAILED,CONFIG_INVALID_EXPORT,FILE_READ_FAILED,K8S_YAML_PARSE_FAILED.Tooling:
scripts/verify-errors.mjs(new layer inpnpm verify) — fails CI if any catalog entry has noraise(...)call site, any anchor is missing fromdocs/ERRORS.md, or the auto-generated catalog section drifts.scripts/generate-errors-doc.mjs(pnpm gen:errors-doc) — regenerates the catalog section ofdocs/ERRORS.mdbetween<!-- BEGIN/END AUTO-GENERATED -->markers.scripts/lib/errors-doc.mjs— shared rendering so generator and verifier can't disagree.cli/validate.ts:serializeErroris gone; the subcommand usesreportError()directly. The--format=jsonoutput gainsseverity,title,docsUrl,cause(additive — existing consumers stay happy).Commits
Stacked PR
This is 2/3, stacked on
refactor/internal-patterns(PR #1). Merge PR #1 first; this base will auto-update tomain.Next: 3/3
docs/test-taxonomy-and-final—test:unit/integ/e2e/contractscripts +docs/TESTING.md+ final docs refresh.Test plan
pnpm verify— full 9-layer chain (this PR adds layer 8:verify:errors).pnpm test:coverage— 294 passing, branches 80.34% (above 80% floor).ERROR_CATALOGinsrc/errors.ts— severity assignments correct?reportError()shape — is theErrorReportpayload what downstream consumers (Sentry, log aggregators) want?loadViteEnv— confirm no internal consumer remains. CHANGELOG calls it out under### Removed.