safe-migrate 0.4.3

Analyze PostgreSQL migrations for schema and locking risks
Documentation
# Cache and Synchronization

This guide describes safe-migrate-owned cache behavior. It is for maintainers
and contributors; the root README is the user setup guide.

## Responsibilities

`sync` reads PostgreSQL catalogs and writes a versioned `DbCache`. The cache is
the baseline used by analysis to distinguish existing production objects from
objects created inside a migration. It is not a database dump and must never
contain connection credentials.

It does contain sensitive metadata: schema, relation, column, constraint,
index, trigger, function, type, role-grant, dependency, and statistics data.
Treat the file like a schema inventory, not a safe-to-share build artifact.

New cache files store provenance:

- creation time as Unix seconds;
- source database name;
- requested schema list, when filtering was used.

Older compatible caches without this provenance remain readable but are stale.
Freshness is calculated from recorded provenance, never filesystem modification
time.

## Connection boundary

`DATABASE_URL` is read only from the environment. The current build accepts
localhost and Unix-socket PostgreSQL URLs. A remote database must be accessed
through an SSH tunnel terminating locally. Do not add credentials to command
line options, TOML, diagnostics, or cache metadata.

`lint` and `lint-chain` are offline by default. `auto_sync = true` is the sole
opt-in that refreshes before analysis. `--no-cache` bypasses both cache loading
and automatic synchronization.

## Inspection and redaction

`safe-migrate cache inspect --cache <path>` reads a cache locally and prints
format/provenance plus counts. `--json` makes that summary scriptable. It never
prints object, column, role, or dependency names and never reads `DATABASE_URL`.
It can inspect an encrypted cache only when encryption is configured and the
environment key is available; neither the key nor credentials are emitted.

There is intentionally no in-place cache redactor. Removing names or edges
from a serialized baseline can make later analysis misleading. To produce a
lower-sensitivity cache, re-sync from a sanitized database or an explicit,
approved schema scope, inspect the new cache, and dispose of the original using
your normal artifact-retention procedure. For encrypted cache-key rotation,
write a fresh cache with a new `SAFE_MIGRATE_CACHE_KEY`, update the secret
store, then remove the old cache and key according to local policy.

Keep caches out of Git and logs. Use owner-only filesystem permissions where
available, short CI artifact retention, and access controls appropriate for a
schema/dependency snapshot.

## Least-privilege sync role

`sync` only issues read-only `SHOW`, `SELECT`, and catalog/view-function calls.
It queries server/version and search-path values plus `pg_class`,
`pg_namespace`, `pg_attribute`, `pg_attrdef`, `pg_constraint`, `pg_depend`,
`pg_index`, `pg_proc`, `pg_type`, `pg_trigger`, `pg_policy`, `pg_rewrite`,
`pg_stats`, and `pg_stat_user_tables`.

Start with a dedicated `LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION`
role, `CONNECT` on the target database, and `USAGE` only on schemas included in
the sync scope. Do not grant write privileges, database ownership,
`pg_read_all_data`, `pg_monitor`, server-file roles, or broad table `SELECT`
by default. PostgreSQL limits `pg_stats` to readable tables, so this minimal
role can yield unknown column widths; that is safer than granting raw data
access solely to improve a heuristic. If a team requires richer widths, grant
`SELECT` only to reviewed relations/columns and record that exception. The
optional `pg_read_all_stats` role reveals broader server statistics and must be
approved separately.

## Write and failure semantics

Synchronize into a temporary file beside the destination, then atomically
replace the destination only after the compressed payload is complete. A sync
failure must not remove or corrupt a previous cache.

When automatic sync fails, report the underlying failure and load the previous
readable cache. Its confidence is determined by cache freshness and analysis,
not by the refresh failure alone. With no readable cache, analysis continues
against an unavailable baseline and is tainted.

## Encryption

When `cache_encryption = true`, cache bytes are encrypted with
XChaCha20-Poly1305. Key material comes only from `SAFE_MIGRATE_CACHE_KEY` as
64 hexadecimal characters (32 bytes). Each write uses a fresh nonce.

The encrypted envelope is authenticated. Missing configuration, missing key
material, an incorrect key, or modified ciphertext must fail closed. Never add
fallback decryption, key storage in TOML, or a command-line key option.

## Change checklist

Changes to cache layout, provenance, encryption, or synchronization behavior
need:

1. versioned decode coverage, including compatible legacy caches;
2. atomic-write and failure-preservation tests;
3. CLI JSON/provenance coverage where user-visible;
4. encryption round-trip and rejection-path tests when applicable;
5. updates to `docs/CONTRACT.md`, the root README, and `CHANGELOG.md` for
   behavior changes.