SIGN IN SIGN UP

refactor: surface error causes across config, storage, and keychain (desloppify C10)

Stops five read paths from silently collapsing failure modes into null.
Each path now distinguishes 'file absent' (returns null, silent) from
'file exists but broken' (logs root cause, returns null or throws typed
error). Callers can act on the distinction instead of guessing.

src/fingerprint/schema.ts
- Add ManifestValidationError (extends Error) so callers can discriminate
  via instanceof without string-matching.
- Replace all 16 bare 'throw new Error(...)' sites in
  validateCandidateManifest and validateVerifiedManifest with
  ManifestValidationError.

src/fingerprint/loader.ts
- readJsonFile now throws ManifestValidationError for malformed JSON (with
  cause chain) and keeps returning null only for ENOENT.
- loadCandidateManifest, loadVerifiedManifest, and loadManifestIndex catch
  ManifestValidationError, log via new logManifestValidationFailure (tier,
  subject, root cause), negative-cache, and return null. Unrelated throws
  propagate.

src/storage.ts
- loadAccounts: split the catch-all into a read step and a parse step. Both
  return null, but non-ENOENT read errors and JSON parse errors now go
  through logStorageReadFailure(path, error) with errno code before the null
  return, so operators notice EACCES/EISDIR/corrupt-storage instead of a
  silent empty-accounts startup.

src/config.ts
- Add ConfigCorruptReadError (extends Error, message carries the refused
  path). saveConfig now reads the existing file via the new readRawConfig
  helper; if readOK=false (file present but unparseable), it throws
  ConfigCorruptReadError instead of merging updates onto {} and clobbering
  the corrupt file with a partial write.
- loadConfig and loadRawConfig route through readRawConfig and call
  logConfigReadFailure on non-ENOENT failures with errno code + message.

src/cc-credentials.ts
- runSecurityCommand now tags each failure path (handled exit code,
  ETIMEDOUT/SIGTERM timeout, unexpected error with status/code/signal
  detail) and calls logSecurityCommandFailure gated on
  OPENCODE_ANTHROPIC_DEBUG=1. Command argv is redacted to the first
  token via redactCommand so the -w keychain-read flag and service names
  never leak to logs. Behavior stays null-on-failure; only observability
  changes.

Tests
- schema.test.ts: 2 new instanceof assertions (+1 regression lock that
  error.name === 'ManifestValidationError').
- loader.test.ts: 1 new test proving ENOENT returns null without throwing.
- config.test.ts: 5 new tests pinning saveConfig corrupt-refuse contract
  (invalid JSON, array, fresh install, happy path, error message carries
  the refused path).
- cc-credentials.test.ts: 4 new tests covering DEBUG=1 log content for
  handled-exit/timeout, silent-default, and argv redaction (no
  'Claude Code-credentials' or '-w' in log output).

Scoped suites green: 41 fingerprint, 62 config, 34 storage, 20 cc-credentials.
Resolves desloppify cluster error-surface-improvements (5/5 members, 3/3
steps). No runtime behavior change on the success path; only error paths
gain diagnostics.
V
Vacbo committed
625d18e37eb50856c01ebd4edc489b86aa244def
Parent: 844c7cc